[web] Fix styles to comply with Bulma 1.0

This commit is contained in:
Alain Nussbaumer
2025-02-04 22:00:48 +01:00
parent b2fbbd3fa0
commit 8140e008f0
117 changed files with 3064 additions and 3606 deletions

View File

@@ -64,5 +64,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,5 +1,5 @@
<template>
<a class="navbar-item" :href="href" @click.stop.prevent="open">
<a :href="href" @click.stop.prevent="open">
<slot />
</a>
</template>
@@ -8,7 +8,7 @@
import { useUIStore } from '@/stores/ui'
export default {
name: 'NavbarItemLink',
name: 'ControlLink',
props: {
to: { required: true, type: Object }
},

View File

@@ -0,0 +1,62 @@
<template>
<div class="media is-align-items-center mb-0">
<div class="media-left">
<a class="button is-small" @click="toggle">
<mdicon class="icon" :name="icon" />
</a>
</div>
<div class="media-content">
<div class="is-size-7 is-uppercase" v-text="$t('navigation.volume')" />
<control-slider
v-model:value="player.volume"
:cursor="cursor"
:max="100"
@change="changeVolume"
/>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import { mdiCancel } from '@mdi/js'
import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'ControlVolume',
components: { ControlSlider },
setup() {
return {
player: usePlayerStore()
}
},
data() {
return {
cursor: mdiCancel,
old_volume: 0
}
},
computed: {
icon() {
return this.player.volume > 0 ? 'volume-high' : 'volume-off'
}
},
watch: {
'player.volume'() {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
methods: {
changeVolume(value) {
webapi.player_volume(this.player.volume)
},
toggle() {
this.player.volume = this.player.volume > 0 ? 0 : this.old_volume
this.changeVolume()
}
}
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div class="media is-align-items-center mb-0">
<div class="media-left">
<a
class="button is-small"
:class="{ 'has-text-grey-light': !output.selected }"
@click="toggle"
>
<mdicon class="icon" :name="icon" :title="output.type" />
</a>
</div>
<div class="media-content">
<div
class="is-size-7 is-uppercase"
:class="{ 'has-text-grey-light': !output.selected }"
v-text="output.name"
/>
<control-slider
v-model:value="volume"
:disabled="!output.selected"
:max="100"
:cursor="cursor"
@change="changeVolume"
/>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import { mdiCancel } from '@mdi/js'
import webapi from '@/webapi'
export default {
name: 'ControlOutputVolume',
components: {
ControlSlider
},
props: { output: { required: true, type: Object } },
data() {
return {
cursor: mdiCancel,
volume: this.output.selected ? this.output.volume : 0
}
},
computed: {
icon() {
if (this.output.type.startsWith('AirPlay')) {
return 'cast-variant'
} else if (this.output.type === 'Chromecast') {
return 'cast'
} else if (this.output.type === 'fifo') {
return 'pipe'
}
return 'server'
}
},
watch: {
output() {
this.volume = this.output.volume
}
},
methods: {
changeVolume() {
webapi.player_output_volume(this.output.id, this.volume)
},
toggle() {
const values = {
selected: !this.output.selected
}
webapi.output_update(this.output.id, values)
}
}
}
</script>

View File

@@ -1,8 +1,8 @@
<template>
<a v-if="visible" :disabled="disabled" @click="seek">
<mdicon
class="icon"
name="rewind-10"
:size="icon_size"
:title="$t('player.button.seek-backward')"
/>
</a>
@@ -14,10 +14,9 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonSeekBack',
name: 'ControlPlayerBack',
props: {
icon_size: { default: 16, type: Number },
seek_ms: { required: true, type: Number }
offset: { required: true, type: Number }
},
setup() {
@@ -52,7 +51,7 @@ export default {
methods: {
seek() {
if (!this.disabled) {
webapi.player_seek(this.seek_ms * -1)
webapi.player_seek(this.offset * -1)
}
}
}

View File

@@ -1,9 +1,9 @@
<template>
<a :class="{ 'is-info': is_consume }" @click="toggle_consume_mode">
<a :class="{ 'is-info': is_consume }" @click="toggle">
<mdicon
class="icon"
name="fire"
:size="icon_size"
size="16"
:title="$t('player.button.consume')"
/>
</a>
@@ -14,10 +14,7 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonConsume',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerConsume',
setup() {
return {
@@ -32,11 +29,9 @@ export default {
},
methods: {
toggle_consume_mode() {
toggle() {
webapi.player_consume(!this.is_consume)
}
}
}
</script>
<style></style>

View File

@@ -1,8 +1,8 @@
<template>
<a v-if="visible" :disabled="disabled" @click="seek">
<mdicon
class="icon"
name="fast-forward-30"
:size="icon_size"
:title="$t('player.button.seek-forward')"
/>
</a>
@@ -14,10 +14,9 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonSeekForward',
name: 'ControlPlayerForward',
props: {
icon_size: { default: 16, type: Number },
seek_ms: { required: true, type: Number }
offset: { required: true, type: Number }
},
setup() {
@@ -52,7 +51,7 @@ export default {
methods: {
seek() {
if (!this.disabled) {
webapi.player_seek(this.seek_ms)
webapi.player_seek(this.offset)
}
}
}

View File

@@ -1,9 +1,9 @@
<template>
<a :class="{ 'is-info': is_active }" @click="toggle_lyrics">
<a :class="{ 'is-info': is_active }" @click="toggle">
<mdicon
class="icon"
:name="icon_name"
:size="icon_size"
:name="icon"
:size="16"
:title="$t('player.button.toggle-lyrics')"
/>
</a>
@@ -13,10 +13,7 @@
import { useLyricsStore } from '@/stores/lyrics'
export default {
name: 'PlayerButtonLyrics',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerLyrics',
setup() {
return {
@@ -25,7 +22,7 @@ export default {
},
computed: {
icon_name() {
icon() {
return this.is_active ? 'script-text-play' : 'script-text-outline'
},
is_active() {
@@ -34,11 +31,9 @@ export default {
},
methods: {
toggle_lyrics() {
toggle() {
this.lyricsStore.pane = !this.lyricsStore.pane
}
}
}
</script>
<style></style>

View File

@@ -1,8 +1,8 @@
<template>
<a :disabled="disabled" @click="play_next">
<mdicon
class="icon"
name="skip-forward"
:size="icon_size"
:title="$t('player.button.skip-forward')"
/>
</a>
@@ -13,10 +13,7 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonNext',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerNext',
computed: {
disabled() {
@@ -29,11 +26,8 @@ export default {
if (this.disabled) {
return
}
webapi.player_next()
}
}
}
</script>
<style></style>

View File

@@ -1,10 +1,6 @@
<template>
<a :disabled="disabled" @click="toggle_play_pause">
<mdicon
:name="icon_name"
:size="icon_size"
:title="$t(`player.button.${icon_name}`)"
/>
<a :disabled="disabled" @click="toggle">
<mdicon class="icon" :name="icon" :title="$t(`player.button.${icon}`)" />
</a>
</template>
@@ -15,9 +11,8 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonPlayPause',
name: 'ControlPlayerPlay',
props: {
icon_size: { default: 16, type: Number },
show_disabled_message: Boolean
},
@@ -33,7 +28,7 @@ export default {
disabled() {
return this.queueStore?.count <= 0
},
icon_name() {
icon() {
if (!this.is_playing) {
return 'play'
} else if (this.is_pause_allowed) {
@@ -51,7 +46,7 @@ export default {
},
methods: {
toggle_play_pause() {
toggle() {
if (this.disabled) {
if (this.show_disabled_message) {
this.notificationsStore.add({
@@ -74,5 +69,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,8 +1,8 @@
<template>
<a :disabled="disabled" @click="play_previous">
<mdicon
class="icon"
name="skip-backward"
:size="icon_size"
:title="$t('player.button.skip-backward')"
/>
</a>
@@ -13,10 +13,7 @@ import { useQueueStore } from '@/stores/queue'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonPrevious',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerPrevious',
setup() {
return {
@@ -40,5 +37,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,10 +1,10 @@
<template>
<a :class="{ 'is-info': !is_repeat_off }" @click="toggle_repeat_mode">
<a :class="{ 'is-info': !is_repeat_off }" @click="toggle">
<mdicon
class="icon"
:name="icon_name"
:size="icon_size"
:title="$t(`player.button.${icon_name}`)"
:name="icon"
:size="16"
:title="$t(`player.button.${icon}`)"
/>
</a>
</template>
@@ -14,19 +14,14 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonRepeat',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerRepeat',
setup() {
return {
playerStore: usePlayerStore()
}
},
computed: {
icon_name() {
icon() {
if (this.is_repeat_all) {
return 'repeat'
} else if (this.is_repeat_single) {
@@ -46,7 +41,7 @@ export default {
},
methods: {
toggle_repeat_mode() {
toggle() {
if (this.is_repeat_all) {
webapi.player_repeat('single')
} else if (this.is_repeat_single) {
@@ -58,5 +53,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,10 +1,10 @@
<template>
<a :class="{ 'is-info': is_shuffle }" @click="toggle_shuffle_mode">
<a :class="{ 'is-info': is_shuffle }" @click="toggle">
<mdicon
class="icon"
:name="icon_name"
:size="icon_size"
:title="$t(`player.button.${icon_name}`)"
:name="icon"
:size="16"
:title="$t(`player.button.${icon}`)"
/>
</a>
</template>
@@ -14,20 +14,14 @@ import { usePlayerStore } from '@/stores/player'
import webapi from '@/webapi'
export default {
name: 'PlayerButtonShuffle',
props: {
icon_size: { default: 16, type: Number }
},
name: 'ControlPlayerShuffle',
setup() {
return {
playerStore: usePlayerStore()
}
},
computed: {
icon_name() {
icon() {
if (this.is_shuffle) {
return 'shuffle'
}
@@ -37,13 +31,10 @@ export default {
return this.playerStore.shuffle
}
},
methods: {
toggle_shuffle_mode() {
toggle() {
webapi.player_shuffle(!this.is_shuffle)
}
}
}
</script>
<style></style>

View File

@@ -0,0 +1,89 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label v-if="$slots.label" class="label has-text-weight-normal">
<slot name="label" />
</label>
<div class="control" :class="{ 'has-icons-right': isSuccess || isError }">
<slot name="input" :setting="setting" :update="update" />
<mdicon
v-if="isSuccess || isError"
class="icon is-right"
:name="isSuccess ? 'check' : 'close'"
size="16"
/>
</div>
<p v-if="$slots.help" class="help">
<slot name="help" />
</p>
</div>
</fieldset>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'ControlSetting',
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
timerDelay: 2000,
timerId: -1
}
},
computed: {
isError() {
return this.timerId === -2
},
isSuccess() {
return this.timerId >= 0
},
setting() {
return this.settingsStore.setting(this.category, this.name)
}
},
methods: {
update(event, sanitise) {
const value = sanitise?.(event.target)
if (value === this.setting.value) {
return
}
const setting = {
category: this.category,
name: this.name,
value
}
webapi
.settings_update(this.category, setting)
.then(() => {
window.clearTimeout(this.timerId)
this.settingsStore.update(setting)
})
.catch(() => {
this.timerId = -2
})
.finally(() => {
this.timerId = window.setTimeout(() => {
this.timerId = -1
}, this.timerDelay)
})
}
}
}
</script>

View File

@@ -0,0 +1,46 @@
<template>
<control-setting
:category="category"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
>
<template #label>
<slot name="label" />
</template>
<template #input="{ setting, update }">
<input
class="input"
inputmode="numeric"
min="0"
:placeholder="placeholder"
:value="setting.value"
@input="update($event, sanitise)"
/>
</template>
<template #help>
<slot name="help" />
</template>
</control-setting>
</template>
<script>
import ControlSetting from '@/components/ControlSetting.vue'
export default {
name: 'ControlSettingIntegerField',
components: { ControlSetting },
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
methods: {
sanitise(target) {
const value = parseInt(target.value.replace(/\D+/gu, ''), 10) || 0
return (target.value = value)
}
}
}
</script>

View File

@@ -0,0 +1,39 @@
<template>
<control-setting :category="category" :disabled="disabled" :name="name">
<template #input="{ setting, update }">
<control-switch
:model-value="setting.value"
@update:model-value="
(value) => update({ target: { checked: value } }, sanitise)
"
>
<template #label>
<slot name="label" />
</template>
<template #help>
<slot name="help" />
</template>
</control-switch>
</template>
</control-setting>
</template>
<script>
import ControlSetting from '@/components/ControlSetting.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
export default {
name: 'ControlSettingSwitch',
components: { ControlSetting, ControlSwitch },
props: {
category: { required: true, type: String },
disabled: { default: false, type: Boolean },
name: { required: true, type: String }
},
methods: {
sanitise(target) {
return target.checked
}
}
}
</script>

View File

@@ -0,0 +1,44 @@
<template>
<control-setting
:category="category"
:disabled="disabled"
:name="name"
:placeholder="placeholder"
>
<template #label>
<slot name="label" />
</template>
<template #input="{ setting, update }">
<input
class="input"
inputmode="text"
:placeholder="placeholder"
:value="setting.value"
@input="update($event, sanitise)"
/>
</template>
<template #help>
<slot name="help" />
</template>
</control-setting>
</template>
<script>
import ControlSetting from '@/components/ControlSetting.vue'
export default {
name: 'ControlSettingTextField',
components: { ControlSetting },
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
methods: {
sanitise(target) {
return target.value
}
}
}
</script>

View File

@@ -32,3 +32,112 @@ export default {
}
}
</script>
<style lang="scss" scoped>
@use 'bulma/sass/utilities/mixins';
@mixin thumb {
-webkit-appearance: none;
width: var(--th);
height: var(--th);
box-sizing: border-box;
border-radius: 50%;
background: var(--bulma-light);
border: 1px solid var(--bulma-grey-lighter);
@media (prefers-color-scheme: dark) {
background: var(--bulma-grey-lighter);
border: 1px solid var(--bulma-grey-dark);
}
}
@mixin thumb-inactive {
box-sizing: border-box;
background-color: var(--bulma-light);
@media (prefers-color-scheme: dark) {
background-color: var(--bulma-grey-dark);
border: 1px solid var(--bulma-grey-darker);
}
}
@mixin track {
height: calc(var(--sh));
border-radius: calc(var(--sh) / 2);
background: linear-gradient(
90deg,
var(--bulma-dark) var(--sx),
var(--bulma-grey-lighter) var(--sx)
);
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
var(--bulma-grey-lighter) var(--sx),
var(--bulma-grey-dark) var(--sx)
);
}
}
@mixin track-inactive {
background: linear-gradient(
90deg,
var(--bulma-grey-light) var(--sx),
var(--bulma-light) var(--sx)
);
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
var(--bulma-grey-dark) var(--sx),
var(--bulma-black-ter) var(--sx)
);
}
}
input[type='range'].slider {
--sh: 0.25rem;
--th: calc(var(--sh) * 4);
background-color: transparent;
@include mixins.mobile {
--th: calc(var(--sh) * 5);
}
& {
--sx: calc(var(--th) / 2 + (var(--ratio) * (100% - var(--th))));
-webkit-appearance: none;
min-width: 250px;
height: calc(var(--sh) * 5);
width: 100% !important;
cursor: grab;
}
&:active {
cursor: grabbing;
}
&::-webkit-slider-thumb {
@include thumb;
& {
margin-top: calc((var(--th) - var(--sh)) / -2);
}
}
&::-moz-range-thumb {
@include thumb;
}
&::-webkit-slider-runnable-track {
@include track;
}
&::-moz-range-track {
@include track;
}
&.is-inactive {
cursor: var(--cursor, not-allowed);
&::-webkit-slider-thumb {
@include thumb-inactive;
}
&::-webkit-slider-runnable-track {
@include track-inactive;
}
&::-moz-range-thumb {
@include thumb-inactive;
}
&::-moz-range-track {
@include track-inactive;
}
}
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<div class="media is-align-items-center mb-0">
<div class="media-left">
<a
class="button is-small"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
>
<mdicon class="icon" name="broadcast" />
</a>
</div>
<div class="media-content is-align-items-center">
<div class="is-flex" :class="{ 'has-text-grey-light': !playing }">
<div class="is-size-7 is-uppercase" v-text="$t('navigation.stream')" />
<a href="stream.mp3" class="ml-2" target="_blank">
<mdicon class="icon is-small" name="open-in-new" />
</a>
</div>
<control-slider
v-model:value="volume"
:cursor="cursor"
:disabled="!playing"
:max="100"
@change="changeVolume"
/>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import audio from '@/lib/Audio'
import { mdiCancel } from '@mdi/js'
export default {
name: 'ControlStreamVolume',
components: { ControlSlider },
emits: ['change', 'mute'],
data() {
return {
cursor: mdiCancel,
loading: false,
playing: false,
volume: 10
}
},
mounted() {
this.setupAudio()
},
unmounted() {
this.closeAudio()
},
methods: {
changeVolume() {
audio.setVolume(this.volume / 100)
},
closeAudio() {
audio.stop()
this.playing = false
},
playChannel() {
if (this.playing) {
return
}
this.loading = true
audio.play('/stream.mp3')
audio.setVolume(this.volume / 100)
},
setupAudio() {
const a = audio.setup()
a.addEventListener('waiting', () => {
this.playing = false
this.loading = true
})
a.addEventListener('playing', () => {
this.playing = true
this.loading = false
})
a.addEventListener('ended', () => {
this.playing = false
this.loading = false
})
a.addEventListener('error', () => {
this.closeAudio()
this.notificationsStore.add({
text: this.$t('navigation.stream-error'),
type: 'danger'
})
this.playing = false
this.loading = false
})
},
togglePlay() {
if (this.loading) {
return
}
if (this.playing) {
this.closeAudio()
}
this.playChannel()
}
}
}
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="field">
<label class="toggle">
<div class="control is-flex is-align-content-center">
<input
:checked="modelValue"
type="checkbox"
class="toggle-checkbox"
@change="$emit('update:modelValue', !modelValue)"
/>
<div class="toggle-switch" />
<slot name="label" />
</div>
</label>
<div v-if="$slots.help" class="help">
<slot name="help" />
</div>
</div>
</template>
<script>
export default {
name: 'ControlSwitch',
props: {
modelValue: Boolean
},
emits: ['update:modelValue']
}
</script>
<style lang="scss" scoped>
.toggle {
cursor: pointer;
display: inline-block;
&-switch {
display: inline-block;
background: var(--bulma-grey-lighter);
border-radius: 1rem;
width: 2.5rem;
height: 1.25rem;
position: relative;
vertical-align: middle;
transition: background 0.25s;
margin-right: 0.5rem;
&:before {
content: '';
display: block;
background: var(--bulma-white);
border-radius: 50%;
width: 1rem;
height: 1rem;
position: absolute;
top: 0.125rem;
left: 0.125rem;
transition: left 0.25s;
}
}
&:hover &-switch:before {
background: var(--bulma-white);
}
&-checkbox {
position: absolute;
visibility: hidden;
&:checked + .toggle-switch {
background: var(--bulma-dark);
&:before {
left: 1.375rem;
}
}
}
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<section>
<nav class="buttons is-centered mb-4 fd-is-square">
<nav class="buttons is-centered mb-4">
<router-link
v-for="index in indices"
:key="index"
class="button is-small"
class="button is-small is-square"
:to="{ hash: `#index_${index}`, query: $route.query }"
>
{{ index }}
@@ -20,4 +20,11 @@ export default {
}
</script>
<style></style>
<style scoped>
.is-square {
height: 1.75rem;
min-width: 1.75rem;
padding-left: 0.25rem;
padding-right: 0.25rem;
}
</style>

View File

@@ -1,37 +1,35 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<cover-artwork
v-if="settingsStore.show_cover_artwork_in_album_lists"
class="media-left"
>
<cover-artwork
:url="item.item.artwork_url"
:artist="item.item.artist"
:album="item.item.name"
class="is-clickable fd-has-shadow fd-cover fd-cover-small-image"
:url="item.item.artwork_url"
:artist="item.item.artist"
:album="item.item.name"
class="media-left fd-has-shadow fd-cover fd-cover-small-image"
/>
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.item.name" />
<div
class="is-size-7 has-text-grey has-text-weight-bold"
v-text="item.item.artist"
/>
<div
v-if="item.item.date_released && item.item.media_kind === 'music'"
class="is-size-7 has-text-grey"
v-text="$filters.date(item.item.date_released)"
/>
</div>
<div class="media-content is-clickable is-clipped">
<div>
<h1 class="title is-6" v-text="item.item.name" />
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
v-text="item.item.artist"
/>
<h2
v-if="item.item.date_released && item.item.media_kind === 'music'"
class="subtitle is-7 has-text-grey"
v-text="$filters.date(item.item.date_released)"
/>
</div>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@@ -49,7 +47,7 @@
@remove-podcast="open_remove_podcast_dialog()"
@play-count-changed="play_count_changed()"
/>
<modal-dialog
<modal-dialog-action
:close_action="$t('page.podcast.cancel')"
:delete_action="$t('page.podcast.remove')"
:show="show_remove_podcast_modal"
@@ -64,20 +62,20 @@
<b v-text="rss_playlist_to_remove.name" />)
</p>
</template>
</modal-dialog>
</modal-dialog-action>
</teleport>
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import ModalDialogAction from '@/components/ModalDialogAction.vue'
import ModalDialogAlbum from '@/components/ModalDialogAlbum.vue'
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'ListAlbums',
components: { CoverArtwork, ModalDialog, ModalDialogAlbum },
components: { CoverArtwork, ModalDialogAction, ModalDialogAlbum },
props: {
items: { required: true, type: Object },
media_kind: { default: '', type: String }
@@ -148,5 +146,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,28 +1,29 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center" @click="open(item)">
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<div
v-if="settingsStore.show_cover_artwork_in_album_lists"
class="media-left is-clickable"
class="media-left"
>
<cover-artwork
:url="artwork_url(item)"
:artist="item.artist"
:album="item.name"
class="is-clickable fd-has-shadow fd-cover fd-cover-small-image"
class="fd-has-shadow fd-cover fd-cover-small-image"
/>
</div>
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.name" />
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.name" />
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.artists[0]?.name"
/>
<h2
class="subtitle is-7 has-text-grey"
v-text="
[item.album_type, $filters.date(item.release_date)].join(', ')
"
<div
class="is-size-7 has-text-grey"
v-text="$filters.date(item.release_date)"
/>
</div>
<div class="media-right">
@@ -76,5 +77,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,17 +1,21 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@@ -60,5 +64,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,8 +1,8 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center">
<div class="media-content is-clickable is-clipped" @click="open(item)">
<h1 class="title is-6" v-text="item.name" />
<div class="media is-align-items-center mb-0">
<div class="media-content is-clickable" @click="open(item)">
<p class="title is-6" v-text="item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@@ -45,5 +45,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,17 +1,21 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@@ -62,5 +66,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,9 +1,11 @@
<template>
<div v-if="$route.query.directory" class="media is-align-items-center">
<figure class="media-left is-clickable" @click="open_parent">
<mdicon class="icon" name="chevron-left" size="16" />
</figure>
<div class="media-content is-clipped">
<div v-if="$route.query.directory" class="media is-align-items-center mb-0">
<mdicon
class="icon media-left is-clickable"
name="chevron-left"
@click="open_parent"
/>
<div class="media-content">
<nav class="breadcrumb">
<ul>
<li v-for="directory in directories" :key="directory.index">
@@ -19,12 +21,13 @@
</div>
</div>
<template v-for="item in items" :key="item.path">
<div class="media is-align-items-center" @click="open(item)">
<figure class="media-left is-clickable">
<mdicon class="icon" name="folder" size="16" />
</figure>
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.name" />
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<mdicon class="media-left icon" name="folder" />
<div class="media-content">
<p class="title is-6" v-text="item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@@ -90,5 +93,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,17 +1,21 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div class="media-content is-clipped">
<div v-if="!item.isItem" class="py-5">
<div class="media-content">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
</div>
<div v-else class="media is-align-items-center" @click="open(item.item)">
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
v-else
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@@ -63,5 +67,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,26 +1,27 @@
<template>
<div
v-if="is_next || !show_only_next_items"
class="media is-align-items-center"
class="media is-align-items-center is-clickable mb-0"
@click="play"
>
<div v-if="edit_mode" class="media-left">
<mdicon
class="icon has-text-grey fd-is-movable handle"
class="icon has-text-grey is-movable"
name="drag-horizontal"
size="16"
size="18"
/>
</div>
<div class="media-content is-clickable is-clipped" @click="play">
<h1
class="title is-6"
<div class="media-content">
<div
class="is-size-6 has-text-weight-bold"
:class="{
'has-text-primary': item.id === player.item_id,
'has-text-grey-light': !is_next
}"
v-text="item.title"
/>
<h2
class="subtitle is-7 has-text-weight-bold"
<div
class="is-size-7 has-text-weight-bold"
:class="{
'has-text-primary': item.id === player.item_id,
'has-text-grey-light': !is_next,
@@ -28,8 +29,8 @@
}"
v-text="item.artist"
/>
<h2
class="subtitle is-7"
<div
class="is-size-7"
:class="{
'has-text-primary': item.id === player.item_id,
'has-text-grey-light': !is_next,
@@ -81,4 +82,8 @@ export default {
}
</script>
<style></style>
<style scoped>
.is-movable {
cursor: move;
}
</style>

View File

@@ -1,11 +1,12 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div class="media is-align-items-center" @click="open(item.item)">
<figure class="media-left is-clickable">
<mdicon class="icon" :name="icon_name(item.item)" size="16" />
</figure>
<div class="media-content is-clickable is-clipped">
<h1 class="title is-6" v-text="item.item.name" />
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item.item)"
>
<mdicon class="media-left icon" :name="icon(item.item)" />
<div class="media-content">
<p class="title is-6" v-text="item.item.name" />
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)">
@@ -39,7 +40,7 @@ export default {
},
methods: {
icon_name(item) {
icon(item) {
if (item.type === 'folder') {
return 'folder'
} else if (item.type === 'rss') {
@@ -61,5 +62,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,9 +1,15 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center">
<div class="media-content is-clickable is-clipped" @click="open(item)">
<h1 class="title is-6" v-text="item.name" />
<h2 class="subtitle is-7" v-text="item.owner.display_name" />
<div
class="media is-align-items-center is-clickable mb-0"
@click="open(item)"
>
<div class="media-content">
<div class="is-size-6 has-text-weight-bold" v-text="item.name" />
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.owner.display_name"
/>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@@ -46,5 +52,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,35 +1,33 @@
<template>
<template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<div v-if="!item.isItem" class="py-5">
<span
:id="`index_${item.index}`"
class="tag is-info is-light is-small has-text-weight-bold"
class="tag is-small has-text-weight-bold"
v-text="item.index"
/>
</div>
<div
v-else
class="media is-align-items-center"
class="media is-align-items-center is-clickable mb-0"
:class="{ 'with-progress': show_progress }"
@click="play(item.item)"
>
<figure v-if="show_icon" class="media-left is-clickable">
<mdicon class="icon" name="file-outline" size="16" />
</figure>
<div class="media-content is-clickable is-clipped">
<h1
class="title is-6"
<mdicon v-if="show_icon" class="media-left icon" name="file-outline" />
<div class="media-content">
<div
class="is-size-6 has-text-weight-bold"
:class="{
'has-text-grey':
item.item.media_kind === 'podcast' && item.item.play_count > 0
}"
v-text="item.item.title"
/>
<h2
class="subtitle is-7 has-text-grey has-text-weight-bold"
<div
class="is-size-7 has-text-weight-bold has-text-grey"
v-text="item.item.artist"
/>
<h2 class="subtitle is-7 has-text-grey" v-text="item.item.album" />
<div class="is-size-7 has-text-grey" v-text="item.item.album" />
<progress
v-if="show_progress && item.item.seek_ms > 0"
class="progress is-info"
@@ -103,4 +101,8 @@ export default {
.progress {
height: 0.25rem;
}
.media.with-progress {
margin-top: 0.375rem;
}
</style>

View File

@@ -1,29 +1,29 @@
<template>
<template v-for="item in items" :key="item.id">
<div class="media is-align-items-center">
<div class="media is-align-items-center mb-0">
<div
class="media-content is-clipped"
class="media-content"
:class="{
'is-clickable': item.is_playable,
'fd-is-not-allowed': !item.is_playable
'is-not-allowed': !item.is_playable
}"
@click="play(item)"
>
<h1
class="title is-6"
<div
class="is-size-6 has-text-weight-bold"
:class="{ 'has-text-grey-light': !item.is_playable }"
v-text="item.name"
/>
<h2
class="subtitle is-7 has-text-weight-bold"
<div
class="is-size-7 has-text-weight-bold"
:class="{
'has-text-grey': item.is_playable,
'has-text-grey-light': !item.is_playable
}"
v-text="item.artists[0].name"
/>
<h2 class="subtitle is-7 has-text-grey" v-text="item.album.name" />
<h2 v-if="!item.is_playable" class="subtitle is-7">
<div class="is-size-7 has-text-grey" v-text="item.album.name" />
<div v-if="!item.is_playable" class="is-size-7 has-text-grey">
(<span v-text="$t('list.spotify.not-playable-track')" />
<span
v-if="item.restrictions?.reason"
@@ -33,7 +33,7 @@
})
"
/>)
</h2>
</div>
</div>
<div class="media-right">
<a @click.prevent.stop="open_dialog(item)">
@@ -83,4 +83,8 @@ export default {
}
</script>
<style></style>
<style scoped>
.is-not-allowed {
cursor: not-allowed;
}
</style>

View File

@@ -223,4 +223,14 @@ export default {
.lyrics div:last-child {
padding-bottom: calc(25vh - 3rem);
}
/* Lyrics animation */
@keyframes pop-color {
0% {
color: var(--bulma-black);
}
100% {
color: var(--bulma-success);
}
}
</style>

View File

@@ -5,38 +5,13 @@
<div class="modal-content">
<div class="card">
<div class="card-content">
<p v-if="title" class="title is-4" v-text="title" />
<slot name="modal-content" />
<slot name="content" />
</div>
<footer class="card-footer is-clipped">
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="close_action" />
</a>
<a
v-if="delete_action"
class="card-footer-item has-background-danger has-text-white has-text-weight-bold"
@click="$emit('delete')"
>
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="delete_action" />
</a>
<a
v-if="ok_action"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="$emit('ok')"
>
<mdicon class="icon" name="check" size="16" />
<span class="is-size-7" v-text="ok_action" />
</a>
<slot name="footer" />
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
@@ -45,14 +20,35 @@
export default {
name: 'ModalDialog',
props: {
close_action: { default: '', type: String },
delete_action: { default: '', type: String },
ok_action: { default: '', type: String },
show: Boolean,
title: { required: true, type: String }
show: Boolean
},
emits: ['delete', 'close', 'ok']
emits: ['close'],
watch: {
show(value) {
const { classList } = document.querySelector('html')
if (value) {
classList.add('is-clipped')
} else {
classList.remove('is-clipped')
}
}
}
}
</script>
<style></style>
<style scoped>
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p v-if="title" class="title is-4" v-text="title" />
<slot name="modal-content" />
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="close_action" />
</a>
<a
v-if="delete_action"
class="card-footer-item has-background-danger"
@click="$emit('delete')"
>
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="delete_action" />
</a>
<a v-if="ok_action" class="card-footer-item" @click="$emit('ok')">
<mdicon class="icon" name="check" size="16" />
<span class="is-size-7" v-text="ok_action" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
export default {
name: 'ModalDialogAction',
components: { ModalDialog },
props: {
close_action: { default: '', type: String },
delete_action: { default: '', type: String },
ok_action: { default: '', type: String },
show: Boolean,
title: { required: true, type: String }
},
emits: ['delete', 'close', 'ok'],
watch: {
show() {
const { classList } = document.querySelector('html')
if (this.show) {
classList.add('is-clipped')
} else {
classList.remove('is-clipped')
}
}
}
}
</script>

View File

@@ -1,68 +1,55 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<form class="card" @submit.prevent="add_stream">
<div class="card-content">
<p class="title is-4" v-text="$t('dialog.add.rss.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="url_field"
v-model="url"
class="input is-shadowless"
type="url"
pattern="http[s]?://.+"
required
:placeholder="$t('dialog.add.rss.placeholder')"
:disabled="loading"
@input="check_url"
/>
<mdicon class="icon is-left" name="rss" size="16" />
</p>
<p class="help" v-text="$t('dialog.add.rss.help')" />
</div>
</div>
<footer v-if="loading" class="card-footer">
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.add.rss.processing')"
/>
</a>
</footer>
<footer v-else class="card-footer is-clipped">
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="add_stream"
>
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.add')" />
</a>
</footer>
</form>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="$t('dialog.add.rss.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="url_field"
v-model="url"
class="input"
type="url"
pattern="http[s]?://.+"
required
:placeholder="$t('dialog.add.rss.placeholder')"
:disabled="loading"
@input="check_url"
/>
<mdicon class="icon is-left" name="rss" size="16" />
</p>
<p class="help" v-text="$t('dialog.add.rss.help')" />
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template>
<template v-if="loading" #footer>
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.processing')" />
</a>
</template>
<template v-else #footer>
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item"
@click="add_stream"
>
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.rss.add')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAddRss',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close', 'podcast-added'],
@@ -107,5 +94,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,75 +1,64 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<form class="card" @submit.prevent="play">
<div class="card-content">
<p class="title is-4" v-text="$t('dialog.add.stream.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="url_field"
v-model="url"
class="input is-shadowless"
type="url"
pattern="http[s]?://.+"
required
:placeholder="$t('dialog.add.stream.placeholder')"
:disabled="loading"
@input="check_url"
/>
<mdicon class="icon is-left" name="web" size="16" />
</p>
</div>
</div>
<footer v-if="loading" class="card-footer">
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.add.stream.loading')"
/>
</a>
</footer>
<footer v-else class="card-footer is-clipped">
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-text-dark"
@click="add_stream"
>
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.add')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="play"
>
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.play')" />
</a>
</footer>
</form>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<form @submit.prevent="play">
<p class="title is-4" v-text="$t('dialog.add.stream.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="url_field"
v-model="url"
class="input"
type="url"
pattern="http[s]?://.+"
required
:placeholder="$t('dialog.add.stream.placeholder')"
:disabled="loading"
@input="check_url"
/>
<mdicon class="icon is-left" name="web" size="16" />
</p>
</div>
</form>
</template>
<template v-if="loading" #footer>
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.loading')" />
</a>
</template>
<template v-else #footer>
<a class="card-footer-item has-text-dark" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-text-dark"
@click="add_stream"
>
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.add')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-text-dark"
@click="play"
>
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.add.stream.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAddUrlStream',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close'],
@@ -125,5 +114,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,116 +1,107 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<cover-artwork
:url="item.artwork_url"
:artist="item.artist"
:album="item.name"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-5"
/>
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div v-if="media_kind_resolved === 'podcast'" class="buttons">
<a
class="button is-small"
@click="mark_played"
v-text="$t('dialog.album.mark-as-played')"
/>
<a
v-if="item.data_kind === 'url'"
class="button is-small"
@click="$emit('remove-podcast')"
v-text="$t('dialog.album.remove-podcast')"
/>
</div>
<div class="content is-small">
<p v-if="item.artist">
<span class="heading" v-text="$t('dialog.album.artist')" />
<a
class="title is-6 has-text-link"
@click="open_artist"
v-text="item.artist"
/>
</p>
<p v-if="item.date_released">
<span
class="heading"
v-text="$t('dialog.album.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.date_released)"
/>
</p>
<p v-else-if="item.year">
<span class="heading" v-text="$t('dialog.album.year')" />
<span class="title is-6" v-text="item.year" />
</p>
<p>
<span class="heading" v-text="$t('dialog.album.tracks')" />
<span class="title is-6" v-text="item.track_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.album.duration')" />
<span
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.album.type')" />
<span
class="title is-6"
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.album.added-on')" />
<span
class="title is-6"
v-text="$filters.datetime(item.time_added)"
/>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.play')" />
</a>
</footer>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<cover-artwork
:url="item.artwork_url"
:artist="item.artist"
:album="item.name"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-3"
/>
<div v-if="media_kind_resolved === 'podcast'" class="buttons">
<a
class="button is-small"
@click="mark_played"
v-text="$t('dialog.album.mark-as-played')"
/>
<a
v-if="item.data_kind === 'url'"
class="button is-small"
@click="$emit('remove-podcast')"
v-text="$t('dialog.album.remove-podcast')"
/>
</div>
<div v-if="item.artist" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.artist')"
/>
<div class="title is-6">
<a @click="open_artist" v-text="item.artist" />
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div v-if="item.date_released" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.release-date')"
/>
<div class="title is-6" v-text="$filters.date(item.date_released)" />
</div>
<div v-else-if="item.year" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.album.year')" />
<div class="title is-6" v-text="item.year" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.tracks')"
/>
<div class="title is-6" v-text="item.track_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.album.type')" />
<div
class="title is-6"
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.album.added-on')"
/>
<div class="title is-6" v-text="$filters.datetime(item.time_added)" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.album.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAlbum',
components: { CoverArtwork },
components: { ModalDialog, CoverArtwork },
props: {
item: { required: true, type: Object },
media_kind: { default: '', type: String },
@@ -184,5 +175,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,90 +1,66 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<cover-artwork
:url="artwork_url(item)"
:artist="item.artist"
:album="item.name"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-5"
@load="artwork_loaded"
@error="artwork_error"
/>
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span
class="heading"
v-text="$t('dialog.spotify.album.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_artist"
v-text="item.artists[0].name"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.album.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.release_date)"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.album.type')"
/>
<span class="title is-6" v-text="item.album_type" />
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.album.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.album.add-next')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.album.play')"
/>
</a>
</footer>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<cover-artwork
:url="artwork_url(item)"
:artist="item.artist"
:album="item.name"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-3"
@load="artwork_loaded"
@error="artwork_error"
/>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.album.album-artist')"
/>
<div class="title is-6">
<a @click="open_artist" v-text="item.artists[0].name" />
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.album.release-date')"
/>
<div class="title is-6" v-text="$filters.date(item.release_date)" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.album.type')"
/>
<div class="title is-6" v-text="item.album_type" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.album.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.album.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.album.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import CoverArtwork from '@/components/CoverArtwork.vue'
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogAlbumSpotify',
components: { CoverArtwork },
components: { ModalDialog, CoverArtwork },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -133,5 +109,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,68 +1,59 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span class="heading" v-text="$t('dialog.artist.albums')" />
<span class="title is-6" v-text="item.album_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.artist.tracks')" />
<span class="title is-6" v-text="item.track_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.artist.type')" />
<span
class="title is-6"
v-text="$t(`data.kind.${item.data_kind}`)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.artist.added-on')" />
<span
class="title is-6"
v-text="$filters.datetime(item.time_added)"
/>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.play')" />
</a>
</footer>
</div>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.artist.albums')"
/>
<div class="title is-6" v-text="item.album_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.artist.tracks')"
/>
<div class="title is-6" v-text="item.track_count" />
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.artist.type')" />
<div class="title is-6" v-text="$t(`data.kind.${item.data_kind}`)" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.artist.added-on')"
/>
<div class="title is-6" v-text="$filters.datetime(item.time_added)" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.artist.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogArtist',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -89,5 +80,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,72 +1,51 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span
class="heading"
v-text="$t('dialog.spotify.artist.popularity')"
/>
<span
class="title is-6"
v-text="[item.popularity, item.followers.total].join(' / ')"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.artist.genres')"
/>
<span class="title is-6" v-text="item.genres.join(', ')" />
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.artist.add')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.artist.add-next')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.artist.play')"
/>
</a>
</footer>
</div>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.artist.popularity')"
/>
<div
class="title is-6"
v-text="[item.popularity, item.followers.total].join(' / ')"
/>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.artist.genres')"
/>
<div class="title is-6" v-text="item.genres.join(', ')" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.artist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.artist.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.artist.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogArtistSpotify',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -93,5 +72,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,71 +1,62 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a
class="has-text-link"
@click="open_albums"
v-text="item.name"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.composer.albums')" />
<a
class="has-text-link is-6"
@click="open_albums"
v-text="item.album_count"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.composer.tracks')" />
<a
class="has-text-link is-6"
@click="open_tracks"
v-text="item.track_count"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.composer.duration')" />
<span
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.play')" />
</a>
</footer>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open_albums" v-text="item.name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.composer.albums')"
/>
<div class="title is-6">
<a @click="open_albums" v-text="item.album_count" />
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.composer.tracks')"
/>
<div class="title is-6">
<a @click="open_tracks" v-text="item.track_count" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.composer.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.composer.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogComposer',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -105,5 +96,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,45 +1,32 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4" v-text="item" />
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.directory.add-next')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.play')" />
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="item" />
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.directory.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogDirectory',
components: { ModalDialog },
props: { item: { required: true, type: String }, show: Boolean },
emits: ['close'],
@@ -66,5 +53,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,61 +1,58 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span class="heading" v-text="$t('dialog.genre.albums')" />
<span class="title is-6" v-text="item.album_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.genre.tracks')" />
<span class="title is-6" v-text="item.track_count" />
</p>
<p>
<span class="heading" v-text="$t('dialog.genre.duration')" />
<span
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.play')" />
</a>
</footer>
</div>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.genre.albums')"
/>
<div class="title is-6" v-text="item.album_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.genre.tracks')"
/>
<div class="title is-6" v-text="item.track_count" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.genre.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.genre.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogGenre',
components: { ModalDialog },
props: {
item: { required: true, type: Object },
media_kind: { required: true, type: String },
@@ -92,5 +89,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,61 +1,55 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span class="heading" v-text="$t('dialog.playlist.path')" />
<span class="title is-6" v-text="item.path" />
</p>
<p>
<span class="heading" v-text="$t('dialog.playlist.type')" />
<span
class="title is-6"
v-text="$t(`playlist.type.${item.type}`)"
/>
</p>
<p v-if="!item.folder">
<span class="heading" v-text="$t('dialog.playlist.tracks')" />
<span class="title is-6" v-text="item.item_count" />
</p>
</div>
</div>
<footer v-if="!item.folder" class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.play')" />
</a>
</footer>
</div>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.playlist.path')"
/>
<div class="title is-6" v-text="item.path" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.playlist.type')"
/>
<div class="title is-6" v-text="$t(`playlist.type.${item.type}`)" />
</div>
<div v-if="!item.folder" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.playlist.tracks')"
/>
<div class="title is-6" v-text="item.item_count" />
</div>
</template>
<template v-if="!item.folder" #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylist',
components: { ModalDialog },
props: {
item: { required: true, type: Object },
show: Boolean,
@@ -86,5 +80,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,73 +1,56 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<form class="card" @submit.prevent="save">
<div class="card-content">
<p class="title is-4" v-text="$t('dialog.playlist.save.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="playlist_name_field"
v-model="playlist_name"
class="input is-shadowless"
type="text"
pattern=".+"
required
:placeholder="$t('dialog.playlist.save.playlist-name')"
:disabled="loading"
@input="check_name"
/>
<mdicon class="icon is-left" name="file-music" size="16" />
</p>
</div>
</div>
<footer v-if="loading" class="card-footer">
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.playlist.save.saving')"
/>
</a>
</footer>
<footer v-else class="card-footer is-clipped">
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.playlist.save.cancel')"
/>
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="save"
>
<mdicon class="icon" name="content-save" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.playlist.save.save')"
/>
</a>
</footer>
</form>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<form @submit.prevent="save">
<p class="title is-4" v-text="$t('dialog.playlist.save.title')" />
<div class="field">
<p class="control has-icons-left">
<input
ref="playlist_name_field"
v-model="playlist_name"
class="input"
type="text"
pattern=".+"
required
:placeholder="$t('dialog.playlist.save.playlist-name')"
:disabled="loading"
@input="check_name"
/>
<mdicon class="icon is-left" name="file-music" size="16" />
</p>
</div>
</form>
</template>
<template v-if="loading" #footer>
<a class="card-footer-item has-text-dark">
<mdicon class="icon" name="web" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.save.saving')" />
</a>
</template>
<template v-else #footer>
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.save.cancel')" />
</a>
<a
:class="{ 'is-disabled': disabled }"
class="card-footer-item has-text-weight-bold"
@click="save"
>
<mdicon class="icon" name="content-save" size="16" />
<span class="is-size-7" v-text="$t('dialog.playlist.save.save')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylistSave',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close'],
@@ -111,5 +94,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,76 +1,58 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4">
<a class="has-text-link" @click="open" v-text="item.name" />
</p>
<div class="content is-small">
<p>
<span
class="heading"
v-text="$t('dialog.spotify.playlist.owner')"
/>
<span class="title is-6" v-text="item.owner.display_name" />
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.playlist.tracks')"
/>
<span class="title is-6" v-text="item.tracks.total" />
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.playlist.path')"
/>
<span class="title is-6" v-text="item.uri" />
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.playlist.add')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.playlist.add-next')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.playlist.play')"
/>
</a>
</footer>
</div>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4">
<a @click="open" v-text="item.name" />
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.playlist.owner')"
/>
<div class="title is-6" v-text="item.owner.display_name" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.playlist.tracks')"
/>
<div class="title is-6" v-text="item.tracks.total" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.playlist.path')"
/>
<div class="title is-6" v-text="item.uri" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.playlist.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.playlist.add-next')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.playlist.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogPlaylistSpotify',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -97,5 +79,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,146 +1,141 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4" v-text="item.title" />
<p class="subtitle" v-text="item.artist" />
<div class="content is-small">
<p v-if="item.album">
<span class="heading" v-text="$t('dialog.queue-item.album')" />
<a
class="title is-6 has-text-link"
@click="open_album"
v-text="item.album"
/>
</p>
<p v-if="item.album_artist">
<span
class="heading"
v-text="$t('dialog.queue-item.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_album_artist"
v-text="item.album_artist"
/>
</p>
<p v-if="item.composer">
<span
class="heading"
v-text="$t('dialog.queue-item.composer')"
/>
<span class="title is-6" v-text="item.composer" />
</p>
<p v-if="item.year">
<span class="heading" v-text="$t('dialog.queue-item.year')" />
<span class="title is-6" v-text="item.year" />
</p>
<p v-if="item.genre">
<span class="heading" v-text="$t('dialog.queue-item.genre')" />
<a
class="title is-6 has-text-link"
@click="open_genre"
v-text="item.genre"
/>
</p>
<p v-if="item.disc_number">
<span
class="heading"
v-text="$t('dialog.queue-item.position')"
/>
<span
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</p>
<p v-if="item.length_ms">
<span
class="heading"
v-text="$t('dialog.queue-item.duration')"
/>
<span
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.queue-item.path')" />
<span class="title is-6" v-text="item.path" />
</p>
<p>
<span class="heading" v-text="$t('dialog.queue-item.type')" />
<span class="title is-6">
<span
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</span>
</p>
<p v-if="item.samplerate">
<span
class="heading"
v-text="$t('dialog.queue-item.quality')"
/>
<span class="title is-6">
<span v-text="item.type" />
<span
v-if="item.samplerate"
v-text="
$t('dialog.queue-item.samplerate', {
rate: item.samplerate
})
"
/>
<span
v-if="item.channels"
v-text="
$t('dialog.queue-item.channels', {
channels: $filters.channels(item.channels)
})
"
/>
<span
v-if="item.bitrate"
v-text="
$t('dialog.queue-item.bitrate', { rate: item.bitrate })
"
/>
</span>
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="remove">
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="$t('dialog.queue-item.remove')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.queue-item.play')" />
</a>
</footer>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<div class="title is-4" v-text="item.title" />
<div class="subtitle" v-text="item.artist" />
<div v-if="item.album" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.album')"
/>
<div class="title is-6">
<a @click="open_album" v-text="item.album" />
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div v-if="item.album_artist" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.album-artist')"
/>
<div class="title is-6">
<a @click="open_album_artist" v-text="item.album_artist" />
</div>
</div>
<div v-if="item.composer" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.composer')"
/>
<div class="title is-6" v-text="item.composer" />
</div>
<div v-if="item.year" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.year')"
/>
<div class="title is-6" v-text="item.year" />
</div>
<div v-if="item.genre" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.genre')"
/>
<div class="title is-6">
<a @click="open_genre" v-text="item.genre" />
</div>
</div>
<div v-if="item.disc_number" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.position')"
/>
<div
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</div>
<div v-if="item.length_ms" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.path')"
/>
<div class="title is-6" v-text="item.path" />
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.type')"
/>
<div class="title is-6">
<span
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</div>
</div>
<div v-if="item.samplerate" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.queue-item.quality')"
/>
<div class="title is-6">
<span v-text="item.type" />
<span
v-if="item.samplerate"
v-text="
$t('dialog.queue-item.samplerate', {
rate: item.samplerate
})
"
/>
<span
v-if="item.channels"
v-text="
$t('dialog.queue-item.channels', {
channels: $filters.channels(item.channels)
})
"
/>
<span
v-if="item.bitrate"
v-text="$t('dialog.queue-item.bitrate', { rate: item.bitrate })"
/>
</div>
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="remove">
<mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="$t('dialog.queue-item.remove')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.queue-item.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi'
export default {
name: 'ModalDialogQueueItem',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -233,5 +228,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,63 +1,44 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4" v-text="$t('dialog.remote-pairing.title')" />
<form @submit.prevent="kickoff_pairing">
<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>
</form>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="$t('dialog.remote-pairing.title')" />
<form @submit.prevent="kickoff_pairing">
<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>
<footer class="card-footer is-clipped">
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.remote-pairing.cancel')"
/>
</a>
<a
class="card-footer-item has-background-info has-text-white has-text-weight-bold"
@click="kickoff_pairing"
>
<mdicon class="icon" name="cellphone" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.remote-pairing.pair')"
/>
</a>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</form>
</template>
<template #footer>
<a class="card-footer-item has-text-danger" @click="$emit('close')">
<mdicon class="icon" name="cancel" size="16" />
<span class="is-size-7" v-text="$t('dialog.remote-pairing.cancel')" />
</a>
<a class="card-footer-item" @click="kickoff_pairing">
<mdicon class="icon" name="cellphone" size="16" />
<span class="is-size-7" v-text="$t('dialog.remote-pairing.pair')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import { useRemotesStore } from '@/stores/remotes'
import webapi from '@/webapi'
export default {
name: 'ModalDialogRemotePairing',
components: { ModalDialog },
props: { show: Boolean },
emits: ['close'],
@@ -98,5 +79,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,182 +1,181 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4" v-text="item.title" />
<p class="subtitle" v-text="item.artist" />
<div v-if="item.media_kind === 'podcast'" class="buttons">
<a
v-if="item.play_count > 0"
class="button is-small"
@click="mark_new"
v-text="$t('dialog.track.mark-as-new')"
/>
<a
v-if="item.play_count === 0"
class="button is-small"
@click="mark_played"
v-text="$t('dialog.track.mark-as-played')"
/>
</div>
<div class="content is-small">
<p v-if="item.album">
<span class="heading" v-text="$t('dialog.track.album')" />
<a
class="title is-6 has-text-link"
@click="open_album"
v-text="item.album"
/>
</p>
<p v-if="item.album_artist && item.media_kind !== 'audiobook'">
<span
class="heading"
v-text="$t('dialog.track.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_album_artist"
v-text="item.album_artist"
/>
</p>
<p v-if="item.composer">
<span class="heading" v-text="$t('dialog.track.composer')" />
<span class="title is-6" v-text="item.composer" />
</p>
<p v-if="item.date_released">
<span
class="heading"
v-text="$t('dialog.track.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.date_released)"
/>
</p>
<p v-else-if="item.year">
<span class="heading" v-text="$t('dialog.track.year')" />
<span class="title is-6" v-text="item.year" />
</p>
<p v-if="item.genre">
<span class="heading" v-text="$t('dialog.track.genre')" />
<a
class="title is-6 has-text-link"
@click="open_genre"
v-text="item.genre"
/>
</p>
<p v-if="item.disc_number">
<span class="heading" v-text="$t('dialog.track.position')" />
<span
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</p>
<p v-if="item.length_ms">
<span class="heading" v-text="$t('dialog.track.duration')" />
<span
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.track.path')" />
<span class="title is-6" v-text="item.path" />
</p>
<p>
<span class="heading" v-text="$t('dialog.track.type')" />
<span class="title is-6">
<span
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</span>
</p>
<p v-if="item.samplerate">
<span class="heading" v-text="$t('dialog.track.quality')" />
<span class="title is-6">
<span v-text="item.type" />
<span
v-if="item.samplerate"
v-text="
$t('dialog.track.samplerate', {
rate: item.samplerate
})
"
/>
<span
v-if="item.channels"
v-text="
$t('dialog.track.channels', {
channels: $filters.channels(item.channels)
})
"
/>
<span
v-if="item.bitrate"
v-text="$t('dialog.track.bitrate', { rate: item.bitrate })"
/>
</span>
</p>
<p>
<span class="heading" v-text="$t('dialog.track.added-on')" />
<span
class="title is-6"
v-text="$filters.datetime(item.time_added)"
/>
</p>
<p>
<span class="heading" v-text="$t('dialog.track.rating')" />
<span
class="title is-6"
v-text="
$t('dialog.track.rating-value', {
rating: Math.floor(item.rating / 10)
})
"
/>
</p>
<p v-if="item.comment">
<span class="heading" v-text="$t('dialog.track.comment')" />
<span class="title is-6" v-text="item.comment" />
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.play')" />
</a>
</footer>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="item.title" />
<p class="subtitle" v-text="item.artist" />
<div v-if="item.media_kind === 'podcast'" class="buttons">
<a
v-if="item.play_count > 0"
class="button is-small"
@click="mark_new"
v-text="$t('dialog.track.mark-as-new')"
/>
<a
v-if="item.play_count === 0"
class="button is-small"
@click="mark_played"
v-text="$t('dialog.track.mark-as-played')"
/>
</div>
<div v-if="item.album" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.album')" />
<div class="title is-6">
<a @click="open_album" v-text="item.album" />
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div
v-if="item.album_artist && item.media_kind !== 'audiobook'"
class="mb-3"
>
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.album-artist')"
/>
<div class="title is-6">
<a @click="open_album_artist" v-text="item.album_artist" />
</div>
</div>
<div v-if="item.composer" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.composer')"
/>
<div class="title is-6" v-text="item.composer" />
</div>
<div v-if="item.date_released" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.release-date')"
/>
<div class="title is-6" v-text="$filters.date(item.date_released)" />
</div>
<div v-else-if="item.year" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.year')" />
<div class="title is-6" v-text="item.year" />
</div>
<div v-if="item.genre" class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.genre')" />
<div class="title is-6">
<a @click="open_genre" v-text="item.genre" />
</div>
</div>
<div v-if="item.disc_number" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.position')"
/>
<div
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</div>
<div v-if="item.length_ms" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.length_ms)"
/>
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.path')" />
<div class="title is-6" v-text="item.path" />
</div>
<div class="mb-3">
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.type')" />
<div
class="title is-6"
v-text="
`${$t(`media.kind.${item.media_kind}`)} - ${$t(`data.kind.${item.data_kind}`)}`
"
/>
</div>
<div v-if="item.samplerate" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.quality')"
/>
<div class="title is-6">
<span v-text="item.type" />
<span
v-if="item.samplerate"
v-text="
$t('dialog.track.samplerate', {
rate: item.samplerate
})
"
/>
<span
v-if="item.channels"
v-text="
$t('dialog.track.channels', {
channels: $filters.channels(item.channels)
})
"
/>
<span
v-if="item.bitrate"
v-text="$t('dialog.track.bitrate', { rate: item.bitrate })"
/>
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.added-on')"
/>
<div class="title is-6" v-text="$filters.datetime(item.time_added)" />
</div>
<div>
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.rating')"
/>
<div
class="title is-6"
v-text="
$t('dialog.track.rating-value', {
rating: Math.floor(item.rating / 10)
})
"
/>
</div>
<div v-if="item.comment" class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.track.comment')"
/>
<div class="title is-6" v-text="item.comment" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.track.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi'
export default {
name: 'ModalDialogTrack',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close', 'play-count-changed'],
@@ -296,5 +295,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,110 +1,88 @@
<template>
<transition name="fade">
<div v-if="show" class="modal is-active">
<div class="modal-background" @click="$emit('close')" />
<div class="modal-content">
<div class="card">
<div class="card-content">
<p class="title is-4" v-text="item.name" />
<p class="subtitle" v-text="item.artists[0].name" />
<div class="content is-small">
<p>
<span
class="heading"
v-text="$t('dialog.spotify.track.album')"
/>
<a
class="title is-6 has-text-link"
@click="open_album"
v-text="item.album.name"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.track.album-artist')"
/>
<a
class="title is-6 has-text-link"
@click="open_artist"
v-text="item.artists[0].name"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.track.release-date')"
/>
<span
class="title is-6"
v-text="$filters.date(item.album.release_date)"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.track.position')"
/>
<span
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.track.duration')"
/>
<span
class="title is-6"
v-text="$filters.durationInHours(item.duration_ms)"
/>
</p>
<p>
<span
class="heading"
v-text="$t('dialog.spotify.track.path')"
/>
<span class="title is-6" v-text="item.uri" />
</p>
</div>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.track.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.track.add-next')"
/>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span
class="is-size-7"
v-text="$t('dialog.spotify.track.play')"
/>
</a>
</footer>
<modal-dialog :show="show" @close="$emit('close')">
<template #content>
<p class="title is-4" v-text="item.name" />
<p class="subtitle" v-text="item.artists[0].name" />
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.album')"
/>
<div class="title is-6">
<a @click="open_album" v-text="item.album.name" />
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.album-artist')"
/>
<div class="title is-6">
<a @click="open_artist" v-text="item.artists[0].name" />
</div>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.release-date')"
/>
<div
class="title is-6"
v-text="$filters.date(item.album.release_date)"
/>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.position')"
/>
<div
class="title is-6"
v-text="[item.disc_number, item.track_number].join(' / ')"
/>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.duration')"
/>
<div
class="title is-6"
v-text="$filters.durationInHours(item.duration_ms)"
/>
</div>
<div class="mb-3">
<div
class="is-size-7 is-uppercase"
v-text="$t('dialog.spotify.track.path')"
/>
<div class="title is-6" v-text="item.uri" />
</div>
</template>
<template #footer>
<a class="card-footer-item has-text-dark" @click="queue_add">
<mdicon class="icon" name="playlist-plus" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.track.add')" />
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<mdicon class="icon" name="playlist-play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.track.add-next')" />
</a>
<a class="card-footer-item has-text-dark" @click="play">
<mdicon class="icon" name="play" size="16" />
<span class="is-size-7" v-text="$t('dialog.spotify.track.play')" />
</a>
</template>
</modal-dialog>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import webapi from '@/webapi'
export default {
name: 'ModalDialogTrackSpotify',
components: { ModalDialog },
props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'],
@@ -138,5 +116,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,19 +1,19 @@
<template>
<modal-dialog
<modal-dialog-action
:show="show"
:title="$t('dialog.update.title')"
:ok_action="library.updating ? '' : $t('dialog.update.rescan')"
:ok_action="libraryStore.updating ? '' : $t('dialog.update.rescan')"
:close_action="$t('dialog.update.cancel')"
@ok="update_library"
@close="close()"
>
<template #modal-content>
<div v-if="!library.updating">
<p class="mb-3" v-text="$t('dialog.update.info')" />
<div v-if="!libraryStore.updating">
<div v-if="spotify_enabled || rss.tracks > 0" class="field">
<label class="label" v-text="$t('dialog.update.info')" />
<div class="control">
<div class="select is-small">
<select v-model="update_dialog_scan_kind">
<select v-model="libraryStore.update_dialog_scan_kind">
<option value="" v-text="$t('dialog.update.all')" />
<option value="files" v-text="$t('dialog.update.local')" />
<option
@@ -30,32 +30,29 @@
</div>
</div>
</div>
<div class="field">
<input
id="rescan"
v-model="rescan_metadata"
type="checkbox"
class="switch is-rounded is-small"
/>
<label for="rescan" v-text="$t('dialog.update.rescan-metadata')" />
</div>
<control-switch v-model="rescan_metadata">
<template #label>
<span v-text="$t('dialog.update.rescan-metadata')" />
</template>
</control-switch>
</div>
<div v-else>
<p class="mb-3" v-text="$t('dialog.update.progress')" />
</div>
</template>
</modal-dialog>
</modal-dialog-action>
</template>
<script>
import ModalDialog from '@/components/ModalDialog.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import ModalDialogAction from '@/components/ModalDialogAction.vue'
import { useLibraryStore } from '@/stores/library'
import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi'
export default {
name: 'ModalDialogUpdate',
components: { ModalDialog },
components: { ControlSwitch, ModalDialogAction },
props: { show: Boolean },
emits: ['close'],
@@ -73,42 +70,26 @@ export default {
},
computed: {
library() {
return this.libraryStore.$state
},
rss() {
return this.libraryStore.rss
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
},
update_dialog_scan_kind: {
get() {
return this.library.update_dialog_scan_kind
},
set(value) {
this.library.update_dialog_scan_kind = value
}
}
},
methods: {
close() {
this.update_dialog_scan_kind = ''
this.libraryStore.update_dialog_scan_kind = ''
this.$emit('close')
},
update_library() {
if (this.rescan_metadata) {
webapi.library_rescan(this.update_dialog_scan_kind)
webapi.library_rescan(this.libraryStore.update_dialog_scan_kind)
} else {
webapi.library_update(this.update_dialog_scan_kind)
webapi.library_update(this.libraryStore.update_dialog_scan_kind)
}
}
}
}
</script>
<style></style>

View File

@@ -1,250 +1,68 @@
<template>
<nav
class="navbar is-block is-white is-fixed-bottom fd-bottom-navbar"
:class="{
'is-transparent': is_now_playing_page,
'is-dark': !is_now_playing_page
}"
role="navigation"
aria-label="player controls"
class="navbar is-fixed-bottom"
:class="{ 'is-dark': !is_now_playing_page }"
>
<!-- Player menu for desktop -->
<div
class="navbar-item has-dropdown has-dropdown-up is-hidden-touch"
:class="{ 'is-active': show_player_menu }"
>
<div class="navbar-dropdown is-right fd-width-auto">
<div class="navbar-item">
<!-- Outputs: master volume -->
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a class="button is-white is-small" @click="toggle_mute_volume">
<mdicon
class="icon"
:name="player.volume > 0 ? 'volume-high' : 'volume-off'"
size="18"
/>
</a>
</div>
<div class="level-item">
<div>
<p class="heading" v-text="$t('navigation.volume')" />
<control-slider
v-model:value="player.volume"
:max="100"
@change="change_volume"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Outputs: master volume -->
<hr class="my-3" />
<navbar-item-output
v-for="output in outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume -->
<hr class="my-3" />
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a
class="button is-white is-small"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
>
<mdicon class="icon" name="broadcast" size="18" />
</a>
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<div
class="is-flex is-align-content-center"
:class="{ 'has-text-grey-light': !playing }"
>
<p class="heading" v-text="$t('navigation.stream')" />
<a href="stream.mp3" class="heading ml-2" target="_blank">
<mdicon
class="icon is-small"
name="open-in-new"
size="16"
/>
</a>
</div>
<control-slider
v-model:value="stream_volume"
:disabled="!playing"
:max="100"
:cursor="cursor"
@change="change_stream_volume"
/>
</div>
</div>
</div>
</div>
</div>
<hr class="my-3" />
<div class="navbar-item is-justify-content-center">
<div class="buttons has-addons">
<player-button-repeat class="button" />
<player-button-shuffle class="button" />
<player-button-consume class="button" />
<player-button-lyrics class="button" />
</div>
</div>
</div>
</div>
<div class="navbar-brand is-flex-grow-1">
<navbar-item-link :to="{ name: 'queue' }" class="mr-auto">
<mdicon class="icon" name="playlist-play" size="24" />
</navbar-item-link>
<navbar-item-link
v-if="!is_now_playing_page"
:to="{ name: 'now-playing' }"
exact
class="is-expanded is-clipped is-size-7"
>
<div class="fd-is-text-clipped">
<strong v-text="current.title" />
<br />
<span v-text="current.artist" />
<span
v-if="current.album"
v-text="$t('navigation.now-playing', { album: current.album })"
/>
</div>
</navbar-item-link>
<player-button-previous
v-if="is_now_playing_page"
class="navbar-item px-2"
:icon_size="24"
/>
<player-button-seek-back
v-if="is_now_playing_page"
:seek_ms="10000"
class="navbar-item px-2"
:icon_size="24"
/>
<player-button-play-pause
class="navbar-item px-2"
:icon_size="36"
show_disabled_message
/>
<player-button-seek-forward
v-if="is_now_playing_page"
:seek_ms="30000"
class="navbar-item px-2"
:icon_size="24"
/>
<player-button-next
v-if="is_now_playing_page"
class="navbar-item px-2"
:icon_size="24"
/>
<control-link class="navbar-item" :to="{ name: 'queue' }">
<mdicon class="icon" name="playlist-play" />
</control-link>
<template v-if="is_now_playing_page">
<control-player-previous class="navbar-item ml-auto" />
<control-player-back class="navbar-item" :offset="10000" />
<control-player-play class="navbar-item" show_disabled_message />
<control-player-forward class="navbar-item" :offset="30000" />
<control-player-next class="navbar-item mr-auto" />
</template>
<template v-else>
<control-link
:to="{ name: 'now-playing' }"
exact
class="navbar-item is-justify-content-flex-start is-expanded is-clipped is-size-7"
>
<div class="is-text-clipped">
<strong v-text="current.title" />
<br />
<span v-text="current.artist" />
<span
v-if="current.album"
v-text="$t('navigation.now-playing', { album: current.album })"
/>
</div>
</control-link>
<control-player-play class="navbar-item" show_disabled_message />
</template>
<a
class="navbar-item ml-auto"
@click="show_player_menu = !show_player_menu"
class="navbar-item"
@click="uiStore.show_player_menu = !uiStore.show_player_menu"
>
<mdicon
class="icon"
:name="show_player_menu ? 'chevron-down' : 'chevron-up'"
:name="uiStore.show_player_menu ? 'chevron-down' : 'chevron-up'"
/>
</a>
</div>
<!-- Player menu for mobile and tablet -->
<div
class="navbar-menu is-hidden-desktop"
:class="{ 'is-active': show_player_menu }"
>
<div class="navbar-item">
<div class="buttons has-addons is-centered">
<player-button-repeat class="button" />
<player-button-shuffle class="button" />
<player-button-consume class="button" />
<player-button-lyrics class="button" />
</div>
</div>
<hr class="my-3" />
<!-- Outputs: master volume -->
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a class="button is-white is-small" @click="toggle_mute_volume">
<mdicon
class="icon"
:name="player.volume > 0 ? 'volume-high' : 'volume-off'"
size="18"
/>
</a>
<div
class="dropdown is-up is-right"
:class="{ 'is-active': uiStore.show_player_menu }"
>
<div class="dropdown-menu">
<div class="dropdown-content">
<div class="dropdown-item pt-0">
<control-main-volume />
<control-output-volume
v-for="output in outputsStore.outputs"
:key="output.id"
:output="output"
/>
<control-stream-volume />
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<p class="heading" v-text="$t('navigation.volume')" />
<control-slider
v-model:value="player.volume"
:max="100"
@change="change_volume"
/>
</div>
</div>
</div>
</div>
</div>
<hr class="my-3" />
<!-- Outputs: speaker volumes -->
<navbar-item-output
v-for="output in outputs"
:key="output.id"
:output="output"
/>
<!-- Outputs: stream volume -->
<hr class="my-3" />
<div class="navbar-item mb-5">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a
class="button is-white is-small"
:class="{
'has-text-grey-light': !playing && !loading,
'is-loading': loading
}"
@click="togglePlay"
>
<mdicon class="icon" name="radio-tower" size="16" />
</a>
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<div
class="is-flex is-align-content-center"
:class="{ 'has-text-grey-light': !playing }"
>
<p class="heading" v-text="$t('navigation.stream')" />
<a href="stream.mp3" class="heading ml-2" target="_blank">
<mdicon
class="icon is-small"
name="open-in-new"
size="16"
/>
</a>
</div>
<control-slider
v-model:value="stream_volume"
:disabled="!playing"
:max="100"
:cursor="cursor"
@change="change_stream_volume"
/>
<hr class="dropdown-divider" />
<div class="dropdown-item is-flex is-justify-content-center">
<div class="buttons has-addons">
<control-player-repeat class="button" />
<control-player-shuffle class="button" />
<control-player-consume class="button" />
<control-player-lyrics class="button" />
</div>
</div>
</div>
@@ -255,168 +73,66 @@
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import NavbarItemLink from '@/components/NavbarItemLink.vue'
import NavbarItemOutput from '@/components/NavbarItemOutput.vue'
import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue'
import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
import PlayerButtonRepeat from '@/components/PlayerButtonRepeat.vue'
import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack.vue'
import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward.vue'
import PlayerButtonShuffle from '@/components/PlayerButtonShuffle.vue'
import audio from '@/lib/Audio'
import { mdiCancel } from '@mdi/js'
import ControlLink from '@/components/ControlLink.vue'
import ControlMainVolume from '@/components/ControlMainVolume.vue'
import ControlOutputVolume from '@/components/ControlOutputVolume.vue'
import ControlPlayerBack from '@/components/ControlPlayerBack.vue'
import ControlPlayerConsume from '@/components/ControlPlayerConsume.vue'
import ControlPlayerForward from '@/components/ControlPlayerForward.vue'
import ControlPlayerLyrics from '@/components/ControlPlayerLyrics.vue'
import ControlPlayerNext from '@/components/ControlPlayerNext.vue'
import ControlPlayerPlay from '@/components/ControlPlayerPlay.vue'
import ControlPlayerPrevious from '@/components/ControlPlayerPrevious.vue'
import ControlPlayerRepeat from '@/components/ControlPlayerRepeat.vue'
import ControlPlayerShuffle from '@/components/ControlPlayerShuffle.vue'
import ControlStreamVolume from '@/components/ControlStreamVolume.vue'
import { useNotificationsStore } from '@/stores/notifications'
import { useOutputsStore } from '@/stores/outputs'
import { usePlayerStore } from '@/stores/player'
import { useQueueStore } from '@/stores/queue'
import { useUIStore } from '@/stores/ui'
import webapi from '@/webapi'
export default {
name: 'NavbarBottom',
components: {
ControlSlider,
NavbarItemLink,
NavbarItemOutput,
PlayerButtonConsume,
PlayerButtonLyrics,
PlayerButtonNext,
PlayerButtonPlayPause,
PlayerButtonPrevious,
PlayerButtonRepeat,
PlayerButtonSeekBack,
PlayerButtonSeekForward,
PlayerButtonShuffle
ControlLink,
ControlOutputVolume,
ControlMainVolume,
ControlPlayerBack,
ControlPlayerConsume,
ControlPlayerForward,
ControlPlayerLyrics,
ControlPlayerNext,
ControlPlayerPlay,
ControlPlayerPrevious,
ControlPlayerRepeat,
ControlPlayerShuffle,
ControlStreamVolume
},
setup() {
return {
notificationsStore: useNotificationsStore(),
outputsStore: useOutputsStore(),
playerStore: usePlayerStore(),
queueStore: useQueueStore(),
uiStore: useUIStore()
}
},
data() {
return {
cursor: mdiCancel,
loading: false,
old_volume: 0,
playing: false,
show_desktop_outputs_menu: false,
show_outputs_menu: false,
stream_volume: 10
}
},
computed: {
is_now_playing_page() {
return this.$route.name === 'now-playing'
},
current() {
return this.queueStore.current
},
outputs() {
return this.outputsStore.outputs
},
player() {
return this.playerStore
},
show_player_menu: {
get() {
return this.uiStore.show_player_menu
},
set(value) {
this.uiStore.show_player_menu = value
}
}
},
watch: {
'playerStore.volume'() {
if (this.player.volume > 0) {
this.old_volume = this.player.volume
}
}
},
// On app mounted
mounted() {
this.setupAudio()
},
// On app destroyed
unmounted() {
this.closeAudio()
},
methods: {
change_stream_volume() {
audio.setVolume(this.stream_volume / 100)
},
change_volume() {
webapi.player_volume(this.player.volume)
},
closeAudio() {
audio.stop()
this.playing = false
},
on_click_outside_outputs() {
this.show_outputs_menu = false
},
playChannel() {
if (this.playing) {
return
}
this.loading = true
audio.play('/stream.mp3')
audio.setVolume(this.stream_volume / 100)
},
setupAudio() {
const a = audio.setup()
a.addEventListener('waiting', () => {
this.playing = false
this.loading = true
})
a.addEventListener('playing', () => {
this.playing = true
this.loading = false
})
a.addEventListener('ended', () => {
this.playing = false
this.loading = false
})
a.addEventListener('error', () => {
this.closeAudio()
this.notificationsStore.add({
text: this.$t('navigation.stream-error'),
type: 'danger'
})
this.playing = false
this.loading = false
})
},
togglePlay() {
if (this.loading) {
return
}
if (this.playing) {
this.closeAudio()
}
this.playChannel()
},
toggle_mute_volume() {
this.player.volume = this.player.volume > 0 ? 0 : this.old_volume
this.change_volume()
}
}
}
</script>
<style></style>
<style scoped>
.is-text-clipped {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -1,93 +0,0 @@
<template>
<div class="navbar-item">
<div class="level is-mobile">
<div class="level-left is-flex-grow-1">
<div class="level-item is-flex-grow-0">
<a
class="button is-white is-small"
:class="{ 'has-text-grey-light': !output.selected }"
@click="set_enabled"
>
<mdicon
class="icon"
:name="type_class"
size="18"
:title="output.type"
/>
</a>
</div>
<div class="level-item">
<div class="is-flex-grow-1">
<p
class="heading"
:class="{ 'has-text-grey-light': !output.selected }"
v-text="output.name"
/>
<control-slider
v-model:value="volume"
:disabled="!output.selected"
:max="100"
:cursor="cursor"
@change="change_volume"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import ControlSlider from '@/components/ControlSlider.vue'
import { mdiCancel } from '@mdi/js'
import webapi from '@/webapi'
export default {
name: 'NavbarItemOutput',
components: {
ControlSlider
},
props: { output: { required: true, type: Object } },
data() {
return {
cursor: mdiCancel,
volume: this.output.selected ? this.output.volume : 0
}
},
computed: {
type_class() {
if (this.output.type.startsWith('AirPlay')) {
return 'cast-variant'
} else if (this.output.type === 'Chromecast') {
return 'cast'
} else if (this.output.type === 'fifo') {
return 'pipe'
}
return 'server'
}
},
watch: {
output() {
this.volume = this.output.volume
}
},
methods: {
change_volume() {
webapi.player_output_volume(this.output.id, this.volume)
},
set_enabled() {
const values = {
selected: !this.output.selected
}
webapi.output_update(this.output.id, values)
}
}
}
</script>
<style></style>

View File

@@ -1,135 +1,151 @@
<template>
<nav
class="navbar is-light is-fixed-top"
:style="zindex"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<navbar-item-link
<nav class="navbar is-light is-fixed-top" :style="zindex">
<div class="navbar-brand is-flex-grow-1">
<control-link
v-if="settingsStore.show_menu_item_playlists"
class="navbar-item"
:to="{ name: 'playlists' }"
>
<mdicon class="icon" name="music-box-multiple" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_music"
class="navbar-item"
:to="{ name: 'music' }"
>
<mdicon class="icon" name="music" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_podcasts"
class="navbar-item"
:to="{ name: 'podcasts' }"
>
<mdicon class="icon" name="microphone" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_audiobooks"
class="navbar-item"
:to="{ name: 'audiobooks' }"
>
<mdicon class="icon" name="book-open-variant" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_radio"
class="navbar-item"
:to="{ name: 'radio' }"
>
<mdicon class="icon" name="radio" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_files"
class="navbar-item"
:to="{ name: 'files' }"
>
<mdicon class="icon" name="folder-open" size="16" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="settingsStore.show_menu_item_search"
class="navbar-item"
:to="{ name: searchStore.search_source }"
>
<mdicon class="icon" name="magnify" size="16" />
</navbar-item-link>
<div
class="navbar-burger"
:class="{ 'is-active': show_burger_menu }"
@click="show_burger_menu = !show_burger_menu"
</control-link>
<a
class="navbar-item ml-auto"
@click="uiStore.show_burger_menu = !uiStore.show_burger_menu"
>
<span />
<span />
<span />
</div>
</div>
<div class="navbar-menu" :class="{ 'is-active': show_burger_menu }">
<div class="navbar-start" />
<div class="navbar-end">
<!-- Burger menu entries -->
<div
class="navbar-item has-dropdown is-hoverable"
:class="{ 'is-active': show_settings_menu }"
@click="on_click_outside_settings"
>
<a class="navbar-item is-arrowless is-hidden-touch">
<mdicon class="icon" name="menu" size="24" />
</a>
<div class="navbar-dropdown is-right">
<navbar-item-link :to="{ name: 'playlists' }">
<mdicon class="icon" name="music-box-multiple" size="16" />
<mdicon
class="icon"
:name="uiStore.show_burger_menu ? 'close' : 'menu'"
/>
</a>
<div
class="dropdown is-right"
:class="{ 'is-active': uiStore.show_burger_menu }"
>
<div class="dropdown-menu">
<div class="dropdown-content">
<control-link class="dropdown-item" :to="{ name: 'playlists' }">
<span class="icon-text">
<mdicon class="icon" name="music-box-multiple" size="16" />
</span>
<b v-text="$t('navigation.playlists')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music' }">
<mdicon class="icon" name="music" size="16" />
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music' }">
<span class="icon-text">
<mdicon class="icon" name="music" size="16" />
</span>
<b v-text="$t('navigation.music')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music-artists' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music-artists' }">
<span class="pl-5" v-text="$t('navigation.artists')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music-albums' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music-albums' }">
<span class="pl-5" v-text="$t('navigation.albums')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'music-genres' }">
</control-link>
<control-link class="dropdown-item" :to="{ name: 'music-genres' }">
<span class="pl-5" v-text="$t('navigation.genres')" />
</navbar-item-link>
<navbar-item-link
</control-link>
<control-link
v-if="spotify_enabled"
class="dropdown-item"
:to="{ name: 'music-spotify' }"
>
<span class="pl-5" v-text="$t('navigation.spotify')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'podcasts' }">
<mdicon class="icon" name="microphone" size="16" />
</control-link>
<control-link class="dropdown-item" :to="{ name: 'podcasts' }">
<span class="icon-text">
<mdicon class="icon" name="microphone" size="16" />
</span>
<b v-text="$t('navigation.podcasts')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'audiobooks' }">
<mdicon class="icon" name="book-open-variant" size="16" />
</control-link>
<control-link class="dropdown-item" :to="{ name: 'audiobooks' }">
<span class="icon-text">
<mdicon class="icon" name="book-open-variant" size="16" />
</span>
<b v-text="$t('navigation.audiobooks')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'radio' }">
<mdicon class="icon" name="radio" size="16" />
</control-link>
<control-link class="dropdown-item" :to="{ name: 'radio' }">
<span class="icon-text">
<mdicon class="icon" name="radio" size="16" />
</span>
<b v-text="$t('navigation.radio')" />
</navbar-item-link>
<navbar-item-link :to="{ name: 'files' }">
<mdicon class="icon" name="folder-open" size="16" />
</control-link>
<control-link class="dropdown-item" :to="{ name: 'files' }">
<span class="icon-text">
<mdicon class="icon" name="folder-open" size="16" />
</span>
<b v-text="$t('navigation.files')" />
</navbar-item-link>
<navbar-item-link :to="{ name: searchStore.search_source }">
<mdicon class="icon" name="magnify" size="16" />
</control-link>
<control-link
class="dropdown-item"
:to="{ name: searchStore.search_source }"
>
<span class="icon-text">
<mdicon class="icon" name="magnify" size="16" />
</span>
<b v-text="$t('navigation.search')" />
</navbar-item-link>
</control-link>
<hr class="my-3" />
<navbar-item-link :to="{ name: 'settings-webinterface' }">
<control-link
class="dropdown-item"
:to="{ name: 'settings-webinterface' }"
>
{{ $t('navigation.settings') }}
</navbar-item-link>
</control-link>
<a
class="navbar-item"
class="dropdown-item"
@click.stop.prevent="open_update_dialog()"
v-text="$t('navigation.update-library')"
/>
<navbar-item-link :to="{ name: 'about' }">
<control-link class="dropdown-item" :to="{ name: 'about' }">
{{ $t('navigation.about') }}
</navbar-item-link>
</control-link>
</div>
</div>
</div>
</div>
<div
v-show="show_settings_menu"
class="is-overlay"
@@ -139,7 +155,7 @@
</template>
<script>
import NavbarItemLink from '@/components/NavbarItemLink.vue'
import ControlLink from '@/components/ControlLink.vue'
import { useSearchStore } from '@/stores/search'
import { useServicesStore } from '@/stores/services'
import { useSettingsStore } from '@/stores/settings'
@@ -147,7 +163,7 @@ import { useUIStore } from '@/stores/ui'
export default {
name: 'NavbarTop',
components: { NavbarItemLink },
components: { ControlLink },
setup() {
return {
@@ -165,22 +181,6 @@ export default {
},
computed: {
show_burger_menu: {
get() {
return this.uiStore.show_burger_menu
},
set(value) {
this.uiStore.show_burger_menu = value
}
},
show_update_dialog: {
get() {
return this.uiStore.show_update_dialog
},
set(value) {
this.uiStore.show_update_dialog = value
}
},
spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid
},
@@ -203,12 +203,10 @@ export default {
this.show_settings_menu = !this.show_settings_menu
},
open_update_dialog() {
this.show_update_dialog = true
this.uiStore.show_update_dialog = true
this.show_settings_menu = false
this.show_burger_menu = false
this.uiStore.show_burger_menu = false
}
}
}
</script>
<style></style>

View File

@@ -1,108 +0,0 @@
<template>
<div class="field">
<input
:id="setting.name"
v-model="setting.value"
type="checkbox"
class="switch is-rounded mr-2"
@change="update_setting"
/>
<label class="pt-0" :for="setting.name">
<slot name="label" />
</label>
<i
class="is-size-7"
:class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
v-text="info"
/>
<p v-if="$slots['info']" class="help">
<slot name="info" />
</p>
</div>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'SettingsCheckbox',
props: {
category: { required: true, type: String },
name: { required: true, type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
statusUpdate: '',
timerDelay: 2000,
timerId: -1
}
},
computed: {
info() {
if (this.is_success) {
return this.$t('setting.saved')
} else if (this.is_error) {
return this.$t('setting.not-saved')
}
return ''
},
is_error() {
return this.statusUpdate === 'error'
},
is_success() {
return this.statusUpdate === 'success'
},
setting() {
const setting = this.settingsStore.setting(this.category, this.name)
if (!setting) {
return {
category: this.category,
name: this.name,
value: false
}
}
return setting
}
},
methods: {
clear_status() {
if (this.is_error) {
this.setting.value = !this.setting.value
}
this.statusUpdate = ''
},
update_setting() {
this.timerId = -1
const setting = {
category: this.category,
name: this.name,
value: this.setting.value
}
webapi
.settings_update(this.category, setting)
.then(() => {
this.settingsStore.update(setting)
this.statusUpdate = 'success'
})
.catch(() => {
this.statusUpdate = 'error'
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}
}
}
</script>
<style></style>

View File

@@ -1,120 +0,0 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label class="label has-text-weight-normal">
<slot name="label" />
<i
class="is-size-7"
:class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
v-text="info"
/>
</label>
<div class="control">
<input
ref="setting"
class="column input is-one-fifth"
inputmode="numeric"
min="0"
:placeholder="placeholder"
:value="setting.value"
@input="set_update_timer"
/>
</div>
<p v-if="$slots['info']" class="help">
<slot name="info" />
</p>
</div>
</fieldset>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'SettingsIntfield',
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
statusUpdate: '',
timerDelay: 2000,
timerId: -1
}
},
computed: {
info() {
if (this.statusUpdate === 'success') {
return this.$t('setting.saved')
} else if (this.statusUpdate === 'error') {
return this.$t('setting.not-saved')
}
return ''
},
is_error() {
return this.statusUpdate === 'error'
},
is_success() {
return this.statusUpdate === 'success'
},
setting() {
return this.settingsStore.setting(this.category, this.name)
}
},
methods: {
clear_status() {
this.statusUpdate = ''
},
set_update_timer(event) {
event.target.value = event.target.value.replace(/[^0-9]/gu, '')
if (this.timerId > 0) {
window.clearTimeout(this.timerId)
this.timerId = -1
}
this.statusUpdate = ''
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
},
update_setting() {
this.timerId = -1
const newValue = parseInt(this.$refs.setting.value, 10)
if (isNaN(newValue) || newValue === this.value) {
this.statusUpdate = ''
return
}
const setting = {
category: this.category,
name: this.name,
value: newValue
}
webapi
.settings_update(this.category, setting)
.then(() => {
this.settingsStore.update(setting)
this.statusUpdate = 'success'
})
.catch(() => {
this.statusUpdate = 'error'
this.$refs.setting.value = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}
}
}
</script>
<style></style>

View File

@@ -1,121 +0,0 @@
<template>
<fieldset :disabled="disabled">
<div class="field">
<label class="label has-text-weight-normal">
<slot name="label" />
<i
class="is-size-7"
:class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
v-text="info"
/>
</label>
<div class="control">
<input
ref="setting"
class="input"
type="text"
:placeholder="placeholder"
:value="setting.value"
@input="set_update_timer"
/>
</div>
<p v-if="$slots['info']" class="help">
<slot name="info" />
</p>
</div>
</fieldset>
</template>
<script>
import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi'
export default {
name: 'SettingsTextfield',
props: {
category: { required: true, type: String },
disabled: Boolean,
name: { required: true, type: String },
placeholder: { default: '', type: String }
},
setup() {
return {
settingsStore: useSettingsStore()
}
},
data() {
return {
statusUpdate: '',
timerDelay: 2000,
timerId: -1
}
},
computed: {
info() {
if (this.statusUpdate === 'success') {
return this.$t('setting.saved')
} else if (this.statusUpdate === 'error') {
return this.$t('setting.not-saved')
}
return ''
},
is_error() {
return this.statusUpdate === 'error'
},
is_success() {
return this.statusUpdate === 'success'
},
setting() {
return this.settingsStore.setting(this.category, this.name)
}
},
methods: {
clear_status() {
this.statusUpdate = ''
},
set_update_timer() {
if (this.timerId > 0) {
window.clearTimeout(this.timerId)
this.timerId = -1
}
this.statusUpdate = ''
const newValue = this.$refs.setting.value
if (newValue !== this.value) {
this.timerId = window.setTimeout(this.update_setting, this.timerDelay)
}
},
update_setting() {
this.timerId = -1
const newValue = this.$refs.setting.value
if (newValue === this.value) {
this.statusUpdate = ''
return
}
const setting = {
category: this.category,
name: this.name,
value: newValue
}
webapi
.settings_update(this.category, setting)
.then(() => {
this.settingsStore.update(setting)
this.statusUpdate = 'success'
})
.catch(() => {
this.statusUpdate = 'error'
this.$refs.setting.value = this.value
})
.finally(() => {
this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
})
}
}
}
</script>
<style></style>

View File

@@ -1,5 +1,5 @@
<template>
<section class="section fd-tabs-section">
<section class="section tabs-section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
@@ -67,5 +67,3 @@ export default {
name: 'TabsAudiobooks'
}
</script>
<style></style>

View File

@@ -1,5 +1,5 @@
<template>
<section class="section fd-tabs-section">
<section class="section tabs-section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
@@ -129,5 +129,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -53,5 +53,3 @@ export default {
}
}
</script>
<style></style>

View File

@@ -1,5 +1,5 @@
<template>
<section class="section fd-tabs-section">
<section class="section tabs-section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
@@ -64,5 +64,3 @@ export default {
name: 'TabsSettings'
}
</script>
<style></style>