[web] Refactor management of remotes and outputs

This commit is contained in:
Alain Nussbaumer 2025-04-27 16:10:30 +02:00
parent 95de42e6be
commit 786d8cbc09
16 changed files with 172 additions and 153 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<component :is="Component" />
</router-view>
<modal-dialog-remote-pairing
:show="pairingActive"
@close="pairingActive = false"
:show="remotesStore.active"
@close="remotesStore.active = false"
/>
<modal-dialog-update
:show="uiStore.showUpdateDialog"
@ -67,7 +67,6 @@ export default {
},
data() {
return {
pairingActive: false,
timerId: 0
}
},
@ -254,7 +253,6 @@ export default {
updatePairing() {
webapi.pairing().then((data) => {
this.remotesStore.$state = data
this.pairingActive = data.active
})
},
updatePlayerStatus() {

View File

@ -0,0 +1,41 @@
<template>
<div class="field is-grouped">
<div class="control">
<input
ref="input"
v-model="value"
class="input"
inputmode="numeric"
pattern="[\d]{4}"
:placeholder="placeholder"
@input="validate"
/>
</div>
<slot />
</div>
</template>
<script>
export default {
name: 'ControlPinField',
props: {
placeholder: { required: true, type: String }
},
emits: ['input'],
data() {
return { value: '' }
},
mounted() {
setTimeout(() => {
this.$refs.input.focus()
}, 10)
},
methods: {
validate(event) {
const { validity } = event.target
const invalid = validity.patternMismatch || validity.valueMissing
this.$emit('input', this.value, invalid)
}
}
}
</script>

View File

@ -29,10 +29,7 @@ export default {
},
emits: ['input'],
data() {
return {
disabled: true,
value: ''
}
return { value: '' }
},
mounted() {
setTimeout(() => {
@ -42,8 +39,8 @@ export default {
methods: {
validate(event) {
const { validity } = event.target
this.disabled = validity.patternMismatch || validity.valueMissing
this.$emit('input', this.value, this.disabled)
const invalid = validity.patternMismatch || validity.valueMissing
this.$emit('input', this.value, invalid)
}
}
}

View File

@ -7,32 +7,25 @@
>
<template #content>
<form @submit.prevent="pair">
<label class="label" v-text="pairing.remote" />
<div class="field">
<div class="control">
<input
ref="pin_field"
v-model="pairing_req.pin"
class="input"
inputmode="numeric"
pattern="[\d]{4}"
:placeholder="$t('dialog.remote-pairing.pairing-code')"
/>
</div>
</div>
<label class="label" v-text="remoteStore.remote" />
<control-pin-field
:placeholder="$t('dialog.remote-pairing.pairing-code')"
@input="onPinChange"
/>
</form>
</template>
</modal-dialog>
</template>
<script>
import ControlPinField from '@/components/ControlPinField.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import { useRemotesStore } from '@/stores/remotes'
import webapi from '@/webapi'
export default {
name: 'ModalDialogRemotePairing',
components: { ModalDialog },
components: { ControlPinField, ModalDialog },
props: { show: Boolean },
emits: ['close'],
setup() {
@ -40,38 +33,34 @@ export default {
},
data() {
return {
pairing_req: { pin: '' }
disabled: true,
pin: ''
}
},
computed: {
actions() {
return [
{ handler: this.cancel, icon: 'cancel', key: 'actions.cancel' },
{ handler: this.pair, icon: 'cellphone', key: 'actions.pair' }
{
disabled: this.disabled,
handler: this.pair,
icon: 'vector-link',
key: 'actions.pair'
}
]
},
pairing() {
return this.remoteStore.pairing
}
},
watch: {
show() {
if (this.show) {
this.loading = false
// Delay setting the focus on the input field until it is part of the DOM and visible
setTimeout(() => {
this.$refs.pin_field.focus()
}, 10)
}
}
},
methods: {
cancel() {
this.$emit('close')
},
onPinChange(pin, disabled) {
this.pin = pin
this.disabled = disabled
},
pair() {
webapi.pairing_kickoff(this.pairing_req).then(() => {
this.pairing_req.pin = ''
webapi.pairing_kickoff({ pin: this.pin }).then(() => {
this.pin = ''
})
}
}

View File

@ -16,8 +16,8 @@ export default {
to: { name: 'settings-webinterface' }
},
{
key: 'page.settings.tabs.remotes-and-outputs',
to: { name: 'settings-remotes-outputs' }
key: 'page.settings.tabs.devices',
to: { name: 'settings-devices' }
},
{
key: 'page.settings.tabs.artwork',

View File

@ -17,7 +17,6 @@
"remove": "Entfernen",
"rescan": "Neu einlesen",
"save": "Speichern",
"send": "Senden",
"show-more": "Zeige mehr",
"shuffle": "Zufallswiedergabe",
"update": "Neu einlesen",
@ -314,9 +313,9 @@
},
"tabs": {
"artwork": "Artwork",
"devices": "Fernbedienungen und Ausgänge",
"general": "Allgemein",
"online-services": "Online-Services",
"remotes-and-outputs": "Fernbedienungen und Ausgänge"
"online-services": "Online-Services"
}
},
"spotify": {

View File

@ -17,7 +17,6 @@
"remove": "Remove",
"rescan": "Rescan",
"save": "Save",
"send": "Send",
"show-more": "Show more",
"shuffle": "Shuffle",
"update": "Update",
@ -314,9 +313,9 @@
},
"tabs": {
"artwork": "Artwork",
"devices": "Remotes and Outputs",
"general": "General",
"online-services": "Online Services",
"remotes-and-outputs": "Remotes and Outputs"
"online-services": "Online Services"
}
},
"spotify": {

View File

@ -17,7 +17,6 @@
"remove": "Supprimer",
"rescan": "Analyser",
"save": "Enregistrer",
"send": "Envoyer",
"show-more": "Afficher plus",
"shuffle": "Lecture aléatoire",
"update": "Actualiser",
@ -314,9 +313,9 @@
},
"tabs": {
"artwork": "Illustrations",
"devices": "Télécommandes et sorties",
"general": "Général",
"online-services": "Services en ligne",
"remotes-and-outputs": "Télécommandes et sorties"
"online-services": "Services en ligne"
}
},
"spotify": {

View File

@ -17,7 +17,6 @@
"remove": "移除",
"rescan": "重新扫描",
"save": "保存",
"send": "发送",
"show-more": "显示更多",
"shuffle": "随机播放",
"update": "更新",
@ -314,9 +313,9 @@
},
"tabs": {
"artwork": "封面",
"devices": "遥控和输出",
"general": "概览",
"online-services": "在线服务",
"remotes-and-outputs": "遥控和输出"
"online-services": "在线服务"
}
},
"spotify": {

View File

@ -17,7 +17,6 @@
"remove": "移除",
"rescan": "重新掃描",
"save": "儲存",
"send": "發送",
"show-more": "顯示更多",
"shuffle": "隨機播放",
"update": "更新",
@ -314,9 +313,9 @@
},
"tabs": {
"artwork": "封面",
"devices": "遙控和輸出",
"general": "概覽",
"online-services": "在線服務",
"remotes-and-outputs": "遙控和輸出"
"online-services": "在線服務"
}
},
"spotify": {

View File

@ -8,7 +8,6 @@ import {
mdiCancel,
mdiCast,
mdiCastVariant,
mdiCellphone,
mdiCheck,
mdiChevronDown,
mdiChevronLeft,
@ -59,6 +58,7 @@ import {
mdiSpeaker,
mdiSpotify,
mdiStop,
mdiVectorLink,
mdiVolumeHigh,
mdiVolumeOff,
mdiWeb
@ -74,7 +74,6 @@ export const icons = {
mdiCancel,
mdiCast,
mdiCastVariant,
mdiCellphone,
mdiCheck,
mdiChevronDown,
mdiChevronLeft,
@ -125,6 +124,7 @@ export const icons = {
mdiSpeaker,
mdiSpotify,
mdiStop,
mdiVectorLink,
mdiVolumeHigh,
mdiVolumeOff,
mdiWeb

View File

@ -7,32 +7,25 @@
/>
</template>
<template #content>
<div v-if="pairing.active">
<form @submit.prevent="kickoffPairing">
<label class="label has-text-weight-normal content">
<span v-text="$t('page.settings.devices.pairing-request')" />
<b v-text="pairing.remote" />
</label>
<div class="field is-grouped">
<div class="control">
<input
v-model="pairingRequest.pin"
class="input"
inputmode="numeric"
pattern="[\d]{4}"
:placeholder="$t('page.settings.devices.pairing-code')"
/>
</div>
<div class="control">
<button
class="button"
type="submit"
v-text="$t('actions.send')"
/>
</div>
<form v-if="remotesStore.active" @submit.prevent="pairRemote">
<label class="label has-text-weight-normal content">
<span v-text="$t('page.settings.devices.pairing-request')" />
<b v-text="remotesStore.remote" />
</label>
<control-pin-field
:placeholder="$t('dialog.remote-pairing.pairing-code')"
@input="onRemotePinChange"
>
<div class="control">
<button
class="button"
type="submit"
:disabled="remotePairingDisabled"
v-text="$t('actions.verify')"
/>
</div>
</form>
</div>
</control-pin-field>
</form>
<div v-else v-text="$t('page.settings.devices.no-active-pairing')" />
</template>
</content-with-heading>
@ -47,7 +40,7 @@
class="content"
v-text="$t('page.settings.devices.speaker-pairing-info')"
/>
<div v-for="output in outputs" :key="output.id">
<div v-for="output in outputs" :key="output.id" class="field is-grouped">
<control-switch
v-model="output.selected"
@update:model-value="toggleOutput(output.id)"
@ -58,19 +51,12 @@
</control-switch>
<form
v-if="output.needs_auth_key"
class="mb-5"
@submit.prevent="kickoffVerification(output.id)"
@submit.prevent="pairOutput(output.id)"
>
<div class="field is-grouped">
<div class="control">
<input
v-model="verificationRequest.pin"
class="input"
inputmode="numeric"
pattern="[\d]{4}"
:placeholder="$t('page.settings.devices.verification-code')"
/>
</div>
<control-pin-field
:placeholder="$t('page.settings.devices.verification-code')"
@input="onOutputPinChange"
>
<div class="control">
<button
class="button"
@ -78,7 +64,7 @@
v-text="$t('actions.verify')"
/>
</div>
</div>
</control-pin-field>
</form>
</div>
</template>
@ -87,6 +73,7 @@
<script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlPinField from '@/components/ControlPinField.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import HeadingTitle from '@/components/HeadingTitle.vue'
import TabsSettings from '@/components/TabsSettings.vue'
@ -95,31 +82,42 @@ import { useRemotesStore } from '@/stores/remotes'
import webapi from '@/webapi'
export default {
name: 'PageSettingsRemotesOutputs',
components: { ContentWithHeading, ControlSwitch, HeadingTitle, TabsSettings },
name: 'PageSettingsDevices',
components: {
ContentWithHeading,
ControlPinField,
ControlSwitch,
HeadingTitle,
TabsSettings
},
setup() {
return { outputsStore: useOutputsStore(), remotesStore: useRemotesStore() }
},
data() {
return {
pairingRequest: { pin: '' },
verificationRequest: { pin: '' }
outputPin: '',
remotePairingDisabled: true,
remotePin: ''
}
},
computed: {
outputs() {
return this.outputsStore.outputs
},
pairing() {
return this.remotesStore.pairing
}
},
methods: {
kickoffPairing() {
webapi.pairing_kickoff(this.pairingRequest)
pairRemote() {
webapi.pairing_kickoff({ pin: this.remotePin })
},
kickoffVerification(identifier) {
webapi.output_update(identifier, this.verificationRequest)
pairOutput(identifier) {
webapi.output_update(identifier, { pin: this.outputPin })
},
onRemotePinChange(pin, disabled) {
this.remotePin = pin
this.remotePairingDisabled = disabled
},
onOutputPinChange(pin) {
this.outputPin = pin
},
toggleOutput(identifier) {
webapi.output_toggle(identifier)

View File

@ -36,8 +36,8 @@ import PageRadioStreams from '@/pages/PageRadioStreams.vue'
import PageSearchLibrary from '@/pages/PageSearchLibrary.vue'
import PageSearchSpotify from '@/pages/PageSearchSpotify.vue'
import PageSettingsArtwork from '@/pages/PageSettingsArtwork.vue'
import PageSettingsDevices from '@/pages/PageSettingsDevices.vue'
import PageSettingsOnlineServices from '@/pages/PageSettingsOnlineServices.vue'
import PageSettingsRemotesOutputs from '@/pages/PageSettingsRemotesOutputs.vue'
import PageSettingsWebinterface from '@/pages/PageSettingsWebinterface.vue'
const TOP_WITH_TABS = 100
@ -233,9 +233,9 @@ export const router = createRouter({
path: '/settings/online-services'
},
{
component: PageSettingsRemotesOutputs,
name: 'settings-remotes-outputs',
path: '/settings/remotes-outputs'
component: PageSettingsDevices,
name: 'settings-devices',
path: '/settings/devices'
}
],
scrollBehavior(to, from, savedPosition) {

View File

@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
export const useRemotesStore = defineStore('RemotesStore', {
state: () => ({
pairing: {}
active: false,
remote: ''
})
})

View File

@ -241,8 +241,8 @@ export default {
return axios.get('./api/pairing')
},
pairing_kickoff(pairingReq) {
return axios.post('./api/pairing', pairingReq)
pairing_kickoff(request) {
return axios.post('./api/pairing', request)
},
player_consume(state) {