Revert "Merge branch 'master' of github.com:owntone/owntone-server"

This reverts commit bb2a778b46afca0fcd56bda6f02857fad78b14d8, reversing
changes made to f8e2298b677476f46eb67cc4331c18124f92a791.
This commit is contained in:
ejurgensen 2025-01-23 09:31:51 +01:00
parent 614bcaa630
commit 8a177ed48d
114 changed files with 3382 additions and 2603 deletions

View File

@ -1,18 +1,19 @@
{ {
"name": "owntone-web", "name": "owntone-web",
"version": "3.0.0", "version": "2.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "owntone-web", "name": "owntone-web",
"version": "3.0.0", "version": "2.0.1",
"dependencies": { "dependencies": {
"@aacassandra/vue3-progressbar": "^1.0.3", "@aacassandra/vue3-progressbar": "^1.0.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@ts-pro/vue-eternal-loading": "^1.3.1", "@ts-pro/vue-eternal-loading": "^1.3.1",
"axios": "^1.7.4", "axios": "^1.7.4",
"bulma": "^1.0.2", "bulma": "^0.9.4",
"bulma-switch": "^2.0.4",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"mdi-vue": "^3.0.13", "mdi-vue": "^3.0.13",
"pinia": "^2.1.7", "pinia": "^2.1.7",
@ -1701,9 +1702,15 @@
} }
}, },
"node_modules/bulma": { "node_modules/bulma": {
"version": "1.0.3", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.3.tgz", "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz",
"integrity": "sha512-9eVXBrXwlU337XUXBjIIq7i88A+tRbJYAjXQjT/21lwam+5tpvKF0R7dCesre9N+HV9c6pzCNEPKrtgvBBes2g==", "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==",
"license": "MIT"
},
"node_modules/bulma-switch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/bulma-switch/-/bulma-switch-2.0.4.tgz",
"integrity": "sha512-kMu4H0Pr0VjvfsnT6viRDCgptUq0Rvy7y7PX6q+IHg1xUynsjszPjhAdal5ysAlCG5HNO+5YXxeiu92qYGQolw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/callsites": { "node_modules/callsites": {

View File

@ -1,6 +1,6 @@
{ {
"name": "owntone-web", "name": "owntone-web",
"version": "3.0.0", "version": "2.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"serve": "vite --port 3000 --host", "serve": "vite --port 3000 --host",
@ -16,7 +16,8 @@
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@ts-pro/vue-eternal-loading": "^1.3.1", "@ts-pro/vue-eternal-loading": "^1.3.1",
"axios": "^1.7.4", "axios": "^1.7.4",
"bulma": "^1.0.2", "bulma": "^0.9.4",
"bulma-switch": "^2.0.4",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"mdi-vue": "^3.0.13", "mdi-vue": "^3.0.13",
"pinia": "^2.1.7", "pinia": "^2.1.7",

View File

@ -1,24 +1,27 @@
<template> <template>
<navbar-top /> <div id="app">
<vue-progress-bar class="has-background-info" /> <navbar-top />
<router-view v-slot="{ Component }"> <vue-progress-bar class="has-background-info" />
<component :is="Component" /> <router-view v-slot="{ Component }">
</router-view> <component :is="Component" />
<modal-dialog-remote-pairing </router-view>
:show="pairing_active"
@close="pairing_active = false" <modal-dialog-remote-pairing
/> :show="pairing_active"
<modal-dialog-update @close="pairing_active = false"
:show="show_update_dialog" />
@close="show_update_dialog = false" <modal-dialog-update
/> :show="show_update_dialog"
<notification-list v-show="!show_burger_menu" /> @close="show_update_dialog = false"
<navbar-bottom /> />
<div <notification-list v-show="!show_burger_menu" />
v-show="show_burger_menu || show_player_menu" <navbar-bottom />
class="overlay-fullscreen" <div
@click="show_burger_menu = show_player_menu = false" v-show="show_burger_menu || show_player_menu"
/> class="fd-overlay-fullscreen"
@click="show_burger_menu = show_player_menu = false"
/>
</div>
</template> </template>
<script> <script>

View File

@ -1,54 +0,0 @@
<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">
<slot name="content" />
</div>
<footer class="card-footer is-clipped">
<slot name="footer" />
</footer>
</div>
</div>
</div>
</transition>
</template>
<script>
export default {
name: 'BaseModal',
props: {
show: Boolean
},
emits: ['close'],
watch: {
show(value) {
const { classList } = document.querySelector('html')
if (value) {
classList.add('is-clipped')
} else {
classList.remove('is-clipped')
}
}
}
}
</script>
<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

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

View File

@ -1,62 +0,0 @@
<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

@ -1,79 +0,0 @@
<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

@ -32,113 +32,3 @@ export default {
} }
} }
</script> </script>
<style lang="scss" scoped>
@use 'bulma/bulma';
@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

@ -1,107 +0,0 @@
<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

@ -1,79 +0,0 @@
<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" />
<slot name="info" />
</div>
</label>
<p v-if="$slots['help']" class="help notification">
<slot name="help" />
</p>
</div>
</template>
<script>
export default {
name: 'ControlSwitch',
props: {
modelValue: Boolean
},
emits: ['update:modelValue']
}
</script>
<style scoped>
.toggle {
cursor: pointer;
display: inline-block;
}
.toggle-switch {
display: inline-block;
background: var(--bulma-grey-lighter);
border-radius: 1rem;
min-width: 2.5rem;
width: 2.5rem;
height: 1.25rem;
position: relative;
vertical-align: middle;
transition: background 0.25s;
margin-right: 0.5rem;
}
.toggle-switch:before,
.toggle-switch:after {
content: '';
}
.toggle-switch:before {
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;
}
.toggle:hover .toggle-switch:before {
background: var(--bulma-white);
}
.toggle-checkbox:checked + .toggle-switch {
background: var(--bulma-dark);
}
.toggle-checkbox:checked + .toggle-switch:before {
left: 1.375rem;
}
.toggle-checkbox {
position: absolute;
visibility: hidden;
}
</style>

View File

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

View File

@ -1,36 +1,38 @@
<template> <template>
<template v-for="item in items" :key="item.itemId"> <template v-for="item in items" :key="item.itemId">
<div v-if="!item.isItem" class="py-5"> <div v-if="!item.isItem" class="mt-6 mb-5 py-2">
<span <span
:id="`index_${item.index}`" :id="`index_${item.index}`"
class="tag is-small has-text-weight-bold" class="tag is-info is-light is-small has-text-weight-bold"
v-text="item.index" v-text="item.index"
/> />
</div> </div>
<div <div v-else class="media is-align-items-center" @click="open(item.item)">
v-else <div
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" v-if="settingsStore.show_cover_artwork_in_album_lists"
:url="item.item.artwork_url" class="media-left"
:artist="item.item.artist" >
:album="item.item.name" <cover-artwork
class="media-left fd-has-shadow fd-cover fd-cover-small-image" :url="item.item.artwork_url"
/> :artist="item.item.artist"
<div class="media-content"> :album="item.item.name"
<div class="is-size-6 has-text-weight-bold" v-text="item.item.name" /> class="is-clickable fd-has-shadow fd-cover fd-cover-small-image"
<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>
<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"> <div class="media-right">
<a @click.prevent.stop="open_dialog(item.item)"> <a @click.prevent.stop="open_dialog(item.item)">
<mdicon class="icon has-text-dark" name="dots-vertical" size="16" /> <mdicon class="icon has-text-dark" name="dots-vertical" size="16" />
@ -146,3 +148,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,9 @@
<template> <template>
<template v-for="item in items" :key="item.id"> <template v-for="item in items" :key="item.id">
<div <div class="media is-align-items-center">
class="media is-align-items-center is-clickable mb-0" <div class="media-content is-clickable is-clipped" @click="open(item)">
@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-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>
<div class="media-right"> <div class="media-right">
<a @click.prevent.stop="open_dialog(item)"> <a @click.prevent.stop="open_dialog(item)">
@ -52,3 +46,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

@ -1,36 +1,49 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<p v-if="title" class="title is-4" v-text="title" /> <div class="modal-background" @click="$emit('close')" />
<slot name="modal-content" /> <div class="modal-content">
</template> <div class="card">
<template #footer> <div class="card-content">
<a class="card-footer-item has-text-dark" @click="$emit('close')"> <p v-if="title" class="title is-4" v-text="title" />
<mdicon class="icon" name="cancel" size="16" /> <slot name="modal-content" />
<span class="is-size-7" v-text="close_action" /> </div>
</a> <footer class="card-footer is-clipped">
<a <a class="card-footer-item has-text-dark" @click="$emit('close')">
v-if="delete_action" <mdicon class="icon" name="cancel" size="16" />
class="card-footer-item has-background-danger" <span class="is-size-7" v-text="close_action" />
@click="$emit('delete')" </a>
> <a
<mdicon class="icon" name="delete" size="16" /> v-if="delete_action"
<span class="is-size-7" v-text="delete_action" /> class="card-footer-item has-background-danger has-text-white has-text-weight-bold"
</a> @click="$emit('delete')"
<a v-if="ok_action" class="card-footer-item" @click="$emit('ok')"> >
<mdicon class="icon" name="check" size="16" /> <mdicon class="icon" name="delete" size="16" />
<span class="is-size-7" v-text="ok_action" /> <span class="is-size-7" v-text="delete_action" />
</a> </a>
</template> <a
</base-modal> 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>
</footer>
</div>
</div>
<button
class="modal-close is-large"
aria-label="close"
@click="$emit('close')"
/>
</div>
</transition>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
export default { export default {
name: 'ModalDialog', name: 'ModalDialog',
components: { BaseModal },
props: { props: {
close_action: { default: '', type: String }, close_action: { default: '', type: String },
delete_action: { default: '', type: String }, delete_action: { default: '', type: String },
@ -38,16 +51,8 @@ export default {
show: Boolean, show: Boolean,
title: { required: true, type: String } title: { required: true, type: String }
}, },
emits: ['delete', 'close', 'ok'], emits: ['delete', 'close', 'ok']
watch: {
show() {
const { classList } = document.querySelector('html')
if (this.show) {
classList.add('is-clipped')
} else {
classList.remove('is-clipped')
}
}
}
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

@ -1,107 +1,116 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open" v-text="item.name" /> <div class="modal-content">
</div> <div class="card">
<cover-artwork <div class="card-content">
:url="item.artwork_url" <cover-artwork
:artist="item.artist" :url="item.artwork_url"
:album="item.name" :artist="item.artist"
class="fd-has-shadow fd-cover fd-cover-normal-image mb-3" :album="item.name"
/> class="fd-has-shadow fd-cover fd-cover-normal-image mb-5"
<div v-if="media_kind_resolved === 'podcast'" class="buttons"> />
<a <p class="title is-4">
class="button is-small" <a class="has-text-link" @click="open" v-text="item.name" />
@click="mark_played" </p>
v-text="$t('dialog.album.mark-as-played')" <div v-if="media_kind_resolved === 'podcast'" class="buttons">
/> <a
<a class="button is-small"
v-if="item.data_kind === 'url'" @click="mark_played"
class="button is-small" v-text="$t('dialog.album.mark-as-played')"
@click="$emit('remove-podcast')" />
v-text="$t('dialog.album.remove-podcast')" <a
/> v-if="item.data_kind === 'url'"
</div> class="button is-small"
<div v-if="item.artist" class="mb-3"> @click="$emit('remove-podcast')"
<div v-text="$t('dialog.album.remove-podcast')"
class="is-size-7 is-uppercase" />
v-text="$t('dialog.album.artist')" </div>
/> <div class="content is-small">
<div class="title is-6"> <p v-if="item.artist">
<a @click="open_artist" v-text="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>
</div> </div>
</div> </div>
<div v-if="item.date_released" class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.album.release-date')" @click="$emit('close')"
/> />
<div class="title is-6" v-text="$filters.date(item.date_released)" /> </div>
</div> </transition>
<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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import CoverArtwork from '@/components/CoverArtwork.vue' import CoverArtwork from '@/components/CoverArtwork.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogAlbum', name: 'ModalDialogAlbum',
components: { BaseModal, CoverArtwork }, components: { CoverArtwork },
props: { props: {
item: { required: true, type: Object }, item: { required: true, type: Object },
media_kind: { default: '', type: String }, media_kind: { default: '', type: String },
@ -175,3 +184,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,59 +1,68 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open" v-text="item.name" /> <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>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.artist.albums')" @click="$emit('close')"
/> />
<div class="title is-6" v-text="item.album_count" /> </div>
</div> </transition>
<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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogArtist', name: 'ModalDialogArtist',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -80,3 +89,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,51 +1,72 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open" v-text="item.name" /> <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>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.spotify.artist.popularity')" @click="$emit('close')"
/> />
<div </div>
class="title is-6" </transition>
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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogArtistSpotify', name: 'ModalDialogArtistSpotify',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -72,3 +93,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,62 +1,71 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open_albums" v-text="item.name" /> <div class="modal-content">
</div> <div class="card">
<div class="mb-3"> <div class="card-content">
<div <p class="title is-4">
class="is-size-7 is-uppercase" <a
v-text="$t('dialog.composer.albums')" class="has-text-link"
/> @click="open_albums"
<div class="title is-6"> v-text="item.name"
<a @click="open_albums" v-text="item.album_count" /> />
</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>
</div> </div>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.composer.tracks')" @click="$emit('close')"
/> />
<div class="title is-6"> </div>
<a @click="open_tracks" v-text="item.track_count" /> </transition>
</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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogComposer', name: 'ModalDialogComposer',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -96,3 +105,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,58 +1,61 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open" v-text="item.name" /> <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>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.genre.albums')" @click="$emit('close')"
/> />
<div class="title is-6" v-text="item.album_count" /> </div>
</div> </transition>
<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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogGenre', name: 'ModalDialogGenre',
components: { BaseModal },
props: { props: {
item: { required: true, type: Object }, item: { required: true, type: Object },
media_kind: { required: true, type: String }, media_kind: { required: true, type: String },
@ -89,3 +92,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,55 +1,61 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open" v-text="item.name" /> <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>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.playlist.path')" @click="$emit('close')"
/> />
<div class="title is-6" v-text="item.path" /> </div>
</div> </transition>
<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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogPlaylist', name: 'ModalDialogPlaylist',
components: { BaseModal },
props: { props: {
item: { required: true, type: Object }, item: { required: true, type: Object },
show: Boolean, show: Boolean,
@ -80,3 +86,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,58 +1,76 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4"> <div class="modal-background" @click="$emit('close')" />
<a @click="open" v-text="item.name" /> <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>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.spotify.playlist.owner')" @click="$emit('close')"
/> />
<div class="title is-6" v-text="item.owner.display_name" /> </div>
</div> </transition>
<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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogPlaylistSpotify', name: 'ModalDialogPlaylistSpotify',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -79,3 +97,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,141 +1,146 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<div class="title is-4" v-text="item.title" /> <div class="modal-background" @click="$emit('close')" />
<div class="subtitle" v-text="item.artist" /> <div class="modal-content">
<div v-if="item.album" class="mb-3"> <div class="card">
<div <div class="card-content">
class="is-size-7 is-uppercase" <p class="title is-4" v-text="item.title" />
v-text="$t('dialog.queue-item.album')" <p class="subtitle" v-text="item.artist" />
/> <div class="content is-small">
<div class="title is-6"> <p v-if="item.album">
<a @click="open_album" v-text="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>
</div> </div>
</div> </div>
<div v-if="item.album_artist" class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.queue-item.album-artist')" @click="$emit('close')"
/> />
<div class="title is-6"> </div>
<a @click="open_album_artist" v-text="item.album_artist" /> </transition>
</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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import { useServicesStore } from '@/stores/services' import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogQueueItem', name: 'ModalDialogQueueItem',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -228,3 +233,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,181 +1,182 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<p class="title is-4" v-text="item.title" /> <div class="modal-background" @click="$emit('close')" />
<p class="subtitle" v-text="item.artist" /> <div class="modal-content">
<div v-if="item.media_kind === 'podcast'" class="buttons"> <div class="card">
<a <div class="card-content">
v-if="item.play_count > 0" <p class="title is-4" v-text="item.title" />
class="button is-small" <p class="subtitle" v-text="item.artist" />
@click="mark_new" <div v-if="item.media_kind === 'podcast'" class="buttons">
v-text="$t('dialog.track.mark-as-new')" <a
/> v-if="item.play_count > 0"
<a class="button is-small"
v-if="item.play_count === 0" @click="mark_new"
class="button is-small" v-text="$t('dialog.track.mark-as-new')"
@click="mark_played" />
v-text="$t('dialog.track.mark-as-played')" <a
/> v-if="item.play_count === 0"
</div> class="button is-small"
<div v-if="item.album" class="mb-3"> @click="mark_played"
<div class="is-size-7 is-uppercase" v-text="$t('dialog.track.album')" /> v-text="$t('dialog.track.mark-as-played')"
<div class="title is-6"> />
<a @click="open_album" v-text="item.album" /> </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>
</div> </div>
</div> </div>
<div <button
v-if="item.album_artist && item.media_kind !== 'audiobook'" class="modal-close is-large"
class="mb-3" aria-label="close"
> @click="$emit('close')"
<div />
class="is-size-7 is-uppercase" </div>
v-text="$t('dialog.track.album-artist')" </transition>
/>
<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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import SpotifyWebApi from 'spotify-web-api-js' import SpotifyWebApi from 'spotify-web-api-js'
import { useServicesStore } from '@/stores/services' import { useServicesStore } from '@/stores/services'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogTrack', name: 'ModalDialogTrack',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close', 'play-count-changed'], emits: ['close', 'play-count-changed'],
@ -295,3 +296,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,88 +1,110 @@
<template> <template>
<base-modal :show="show" @close="$emit('close')"> <transition name="fade">
<template #content> <div v-if="show" class="modal is-active">
<p class="title is-4" v-text="item.name" /> <div class="modal-background" @click="$emit('close')" />
<p class="subtitle" v-text="item.artists[0].name" /> <div class="modal-content">
<div class="mb-3"> <div class="card">
<div <div class="card-content">
class="is-size-7 is-uppercase" <p class="title is-4" v-text="item.name" />
v-text="$t('dialog.spotify.track.album')" <p class="subtitle" v-text="item.artists[0].name" />
/> <div class="content is-small">
<div class="title is-6"> <p>
<a @click="open_album" v-text="item.album.name" /> <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>
</div> </div>
</div> </div>
<div class="mb-3"> <button
<div class="modal-close is-large"
class="is-size-7 is-uppercase" aria-label="close"
v-text="$t('dialog.spotify.track.album-artist')" @click="$emit('close')"
/> />
<div class="title is-6"> </div>
<a @click="open_artist" v-text="item.artists[0].name" /> </transition>
</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>
</base-modal>
</template> </template>
<script> <script>
import BaseModal from '@/components/BaseModal.vue'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'ModalDialogTrackSpotify', name: 'ModalDialogTrackSpotify',
components: { BaseModal },
props: { item: { required: true, type: Object }, show: Boolean }, props: { item: { required: true, type: Object }, show: Boolean },
emits: ['close'], emits: ['close'],
@ -116,3 +138,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -1,68 +1,250 @@
<template> <template>
<nav <nav
class="navbar is-fixed-bottom" class="navbar is-block is-white is-fixed-bottom fd-bottom-navbar"
:class="{ 'is-dark': !is_now_playing_page }" :class="{
'is-transparent': is_now_playing_page,
'is-dark': !is_now_playing_page
}"
role="navigation"
aria-label="player controls"
> >
<div class="navbar-brand is-flex-grow-1"> <!-- Player menu for desktop -->
<control-link class="navbar-item" :to="{ name: 'queue' }"> <div
<mdicon class="icon" name="playlist-play" /> class="navbar-item has-dropdown has-dropdown-up is-hidden-touch"
</control-link> :class="{ 'is-active': show_player_menu }"
<template v-if="is_now_playing_page"> >
<control-player-previous class="navbar-item ml-auto" /> <div class="navbar-dropdown is-right fd-width-auto">
<control-player-back class="navbar-item" :offset="10000" /> <div class="navbar-item">
<control-player-play class="navbar-item" show_disabled_message /> <!-- Outputs: master volume -->
<control-player-forward class="navbar-item" :offset="30000" /> <div class="level is-mobile">
<control-player-next class="navbar-item mr-auto" /> <div class="level-left is-flex-grow-1">
</template> <div class="level-item is-flex-grow-0">
<template v-else> <a class="button is-white is-small" @click="toggle_mute_volume">
<control-link <mdicon
:to="{ name: 'now-playing' }" class="icon"
exact :name="player.volume > 0 ? 'volume-high' : 'volume-off'"
class="navbar-item is-justify-content-flex-start is-expanded is-clipped is-size-7" size="18"
> />
<div class="is-text-clipped"> </a>
<strong v-text="current.title" /> </div>
<br /> <div class="level-item">
<span v-text="current.artist" /> <div>
<span <p class="heading" v-text="$t('navigation.volume')" />
v-if="current.album" <control-slider
v-text="$t('navigation.now-playing', { album: current.album })" v-model:value="player.volume"
/> :max="100"
@change="change_volume"
/>
</div>
</div>
</div>
</div> </div>
</control-link> </div>
<control-player-play class="navbar-item" show_disabled_message /> <!-- Outputs: master volume -->
</template> <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"
/>
<a <a
class="navbar-item" class="navbar-item ml-auto"
@click="uiStore.show_player_menu = !uiStore.show_player_menu" @click="show_player_menu = !show_player_menu"
> >
<mdicon <mdicon
class="icon" class="icon"
:name="uiStore.show_player_menu ? 'chevron-down' : 'chevron-up'" :name="show_player_menu ? 'chevron-down' : 'chevron-up'"
/> />
</a> </a>
<div </div>
class="dropdown is-up is-right" <!-- Player menu for mobile and tablet -->
:class="{ 'is-active': uiStore.show_player_menu }" <div
> class="navbar-menu is-hidden-desktop"
<div class="dropdown-menu"> :class="{ 'is-active': show_player_menu }"
<div class="dropdown-content"> >
<div class="dropdown-item pt-0"> <div class="navbar-item">
<control-main-volume /> <div class="buttons has-addons is-centered">
<control-output-volume <player-button-repeat class="button" />
v-for="output in outputsStore.outputs" <player-button-shuffle class="button" />
:key="output.id" <player-button-consume class="button" />
:output="output" <player-button-lyrics class="button" />
/> </div>
<control-stream-volume /> </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> </div>
<hr class="dropdown-divider" /> <div class="level-item">
<div class="dropdown-item is-flex is-justify-content-center"> <div class="is-flex-grow-1">
<div class="buttons has-addons"> <p class="heading" v-text="$t('navigation.volume')" />
<control-player-repeat class="button" /> <control-slider
<control-player-shuffle class="button" /> v-model:value="player.volume"
<control-player-consume class="button" /> :max="100"
<control-player-lyrics class="button" /> @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"
/>
</div> </div>
</div> </div>
</div> </div>
@ -73,66 +255,168 @@
</template> </template>
<script> <script>
import ControlLink from '@/components/ControlLink.vue' import ControlSlider from '@/components/ControlSlider.vue'
import ControlMainVolume from '@/components/ControlMainVolume.vue' import NavbarItemLink from '@/components/NavbarItemLink.vue'
import ControlOutputVolume from '@/components/ControlOutputVolume.vue' import NavbarItemOutput from '@/components/NavbarItemOutput.vue'
import ControlPlayerBack from '@/components/ControlPlayerBack.vue' import PlayerButtonConsume from '@/components/PlayerButtonConsume.vue'
import ControlPlayerConsume from '@/components/ControlPlayerConsume.vue' import PlayerButtonLyrics from '@/components/PlayerButtonLyrics.vue'
import ControlPlayerForward from '@/components/ControlPlayerForward.vue' import PlayerButtonNext from '@/components/PlayerButtonNext.vue'
import ControlPlayerLyrics from '@/components/ControlPlayerLyrics.vue' import PlayerButtonPlayPause from '@/components/PlayerButtonPlayPause.vue'
import ControlPlayerNext from '@/components/ControlPlayerNext.vue' import PlayerButtonPrevious from '@/components/PlayerButtonPrevious.vue'
import ControlPlayerPlay from '@/components/ControlPlayerPlay.vue' import PlayerButtonRepeat from '@/components/PlayerButtonRepeat.vue'
import ControlPlayerPrevious from '@/components/ControlPlayerPrevious.vue' import PlayerButtonSeekBack from '@/components/PlayerButtonSeekBack.vue'
import ControlPlayerRepeat from '@/components/ControlPlayerRepeat.vue' import PlayerButtonSeekForward from '@/components/PlayerButtonSeekForward.vue'
import ControlPlayerShuffle from '@/components/ControlPlayerShuffle.vue' import PlayerButtonShuffle from '@/components/PlayerButtonShuffle.vue'
import ControlStreamVolume from '@/components/ControlStreamVolume.vue' import audio from '@/lib/Audio'
import { mdiCancel } from '@mdi/js'
import { useNotificationsStore } from '@/stores/notifications' import { useNotificationsStore } from '@/stores/notifications'
import { useOutputsStore } from '@/stores/outputs' import { useOutputsStore } from '@/stores/outputs'
import { usePlayerStore } from '@/stores/player'
import { useQueueStore } from '@/stores/queue' import { useQueueStore } from '@/stores/queue'
import { useUIStore } from '@/stores/ui' import { useUIStore } from '@/stores/ui'
import webapi from '@/webapi'
export default { export default {
name: 'NavbarBottom', name: 'NavbarBottom',
components: { components: {
ControlLink, ControlSlider,
ControlOutputVolume, NavbarItemLink,
ControlMainVolume, NavbarItemOutput,
ControlPlayerBack, PlayerButtonConsume,
ControlPlayerConsume, PlayerButtonLyrics,
ControlPlayerForward, PlayerButtonNext,
ControlPlayerLyrics, PlayerButtonPlayPause,
ControlPlayerNext, PlayerButtonPrevious,
ControlPlayerPlay, PlayerButtonRepeat,
ControlPlayerPrevious, PlayerButtonSeekBack,
ControlPlayerRepeat, PlayerButtonSeekForward,
ControlPlayerShuffle, PlayerButtonShuffle
ControlStreamVolume
}, },
setup() { setup() {
return { return {
notificationsStore: useNotificationsStore(), notificationsStore: useNotificationsStore(),
outputsStore: useOutputsStore(), outputsStore: useOutputsStore(),
playerStore: usePlayerStore(),
queueStore: useQueueStore(), queueStore: useQueueStore(),
uiStore: useUIStore() 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: { computed: {
is_now_playing_page() { is_now_playing_page() {
return this.$route.name === 'now-playing' return this.$route.name === 'now-playing'
}, },
current() { current() {
return this.queueStore.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> </script>
<style scoped> <style></style>
.is-text-clipped {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,32 @@
<template> <template>
<control-switch v-model="setting.value" @update:model-value="update"> <div class="field">
<template #label> <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" /> <slot name="label" />
</template> </label>
<template #info> <i
<mdicon class="is-size-7"
v-if="isSuccess" :class="{ 'has-text-info': is_success, 'has-text-danger': is_error }"
class="icon has-text-info" v-text="info"
name="check" />
size="16" <p v-if="$slots['info']" class="help">
/> <slot name="info" />
<mdicon </p>
v-if="isError" </div>
class="icon has-text-danger"
name="close"
size="16"
/>
</template>
</control-switch>
</template> </template>
<script> <script>
import ControlSwitch from '@/components/ControlSwitch.vue'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import webapi from '@/webapi' import webapi from '@/webapi'
export default { export default {
name: 'SettingsCheckbox', name: 'SettingsCheckbox',
components: { ControlSwitch },
props: { props: {
category: { required: true, type: String }, category: { required: true, type: String },
name: { required: true, type: String } name: { required: true, type: String }
@ -48,10 +47,18 @@ export default {
}, },
computed: { computed: {
isError() { 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' return this.statusUpdate === 'error'
}, },
isSuccess() { is_success() {
return this.statusUpdate === 'success' return this.statusUpdate === 'success'
}, },
setting() { setting() {
@ -68,13 +75,13 @@ export default {
}, },
methods: { methods: {
clearStatus() { clear_status() {
if (this.is_error) { if (this.is_error) {
this.setting.value = !this.setting.value this.setting.value = !this.setting.value
} }
this.statusUpdate = '' this.statusUpdate = ''
}, },
update() { update_setting() {
this.timerId = -1 this.timerId = -1
const setting = { const setting = {
category: this.category, category: this.category,
@ -91,9 +98,11 @@ export default {
this.statusUpdate = 'error' this.statusUpdate = 'error'
}) })
.finally(() => { .finally(() => {
this.timerId = window.setTimeout(this.clearStatus, this.timerDelay) this.timerId = window.setTimeout(this.clear_status, this.timerDelay)
}) })
} }
} }
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

@ -67,3 +67,5 @@ export default {
name: 'TabsAudiobooks' name: 'TabsAudiobooks'
} }
</script> </script>
<style></style>

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ export const filters = {
.toLocaleString(DateTime.DATETIME_MED) .toLocaleString(DateTime.DATETIME_MED)
}, },
durationInDays(value) { durationInDays(value) {
const minutes = Math.floor(value / 60) const minutes = Math.floor(value / 60000)
if (minutes > 1440) { if (minutes > 1440) {
return Duration.fromObject({ minutes }) return Duration.fromObject({ minutes })
.shiftTo('days', 'hours', 'minutes') .shiftTo('days', 'hours', 'minutes')
@ -53,6 +53,6 @@ export const filters = {
}, },
timeFromNow(value) { timeFromNow(value) {
const diff = DateTime.now().diff(DateTime.fromISO(value)) const diff = DateTime.now().diff(DateTime.fromISO(value))
return this.durationInDays(diff.as('seconds')) return this.durationInDays(diff.as('milliseconds'))
} }
} }

View File

@ -241,7 +241,6 @@
"compiled-with": "Compiliert mit Unterstützung von {options}.", "compiled-with": "Compiliert mit Unterstützung von {options}.",
"library": "Bibliothek", "library": "Bibliothek",
"more": "mehr", "more": "mehr",
"name": "Name",
"total-playtime": "Gesamte Spielzeit", "total-playtime": "Gesamte Spielzeit",
"tracks": "Tracks", "tracks": "Tracks",
"update": "Neu einlesen", "update": "Neu einlesen",
@ -408,8 +407,8 @@
"count": "{count} Track|{count} Track|{count} Tracks", "count": "{count} Track|{count} Track|{count} Tracks",
"edit": "Bearbeiten", "edit": "Bearbeiten",
"hide-previous": "Vorherige verbergen", "hide-previous": "Vorherige verbergen",
"save": "Speichern", "title": "Warteschlange",
"title": "Warteschlange" "save": "Speichern"
}, },
"radio": { "radio": {
"count": "{count} Station|{count} Station|{count} Stationen", "count": "{count} Station|{count} Station|{count} Stationen",
@ -496,7 +495,8 @@
"spotify": { "spotify": {
"no-support": "OwnTone wurde entweder ohne Unterstützung für Spotify erstellt oder libspotify ist nicht installiert.", "no-support": "OwnTone wurde entweder ohne Unterstützung für Spotify erstellt oder libspotify ist nicht installiert.",
"logged-as": "Angemeldet als ", "logged-as": "Angemeldet als ",
"requirements": "Spotify Premium Abo erforderlich. Zugriff auf die Spotify Web-Api ermöglicht scannen der Spotify-Blibliothek. Erforderliche scopes sind: ", "requirements": "Spotify Premium Abo erforderlich.",
"scopes": "Zugriff auf die Spotify Web-Api ermöglicht scannen der Spotify-Blibliothek. Erforderliche scopes sind: ",
"user": "Zugriff gestattet für ", "user": "Zugriff gestattet für ",
"authorize": "Authorisiere Web-API-Zugriff", "authorize": "Authorisiere Web-API-Zugriff",
"grant-access": "Zugriff auf die Spotify Web-API gestatten", "grant-access": "Zugriff auf die Spotify Web-API gestatten",
@ -572,6 +572,10 @@
"toggle-lyrics": "Liedtexte anzeigen/verbergen" "toggle-lyrics": "Liedtexte anzeigen/verbergen"
} }
}, },
"setting": {
"not-saved": " (Fehler beim Speichern der Einstellungen)",
"saved": " (Einstellungen gesichert)"
},
"server": { "server": {
"connection-failed": "Fehler bei Verbindung zum OwnTone-Server", "connection-failed": "Fehler bei Verbindung zum OwnTone-Server",
"request-failed": "Anfrage gescheitert (Status: {status} {cause} {url})", "request-failed": "Anfrage gescheitert (Status: {status} {cause} {url})",

View File

@ -241,7 +241,6 @@
"compiled-with": "Compiled with support for {options}.", "compiled-with": "Compiled with support for {options}.",
"library": "Library", "library": "Library",
"more": "more", "more": "more",
"name": "Name",
"total-playtime": "Total playtime", "total-playtime": "Total playtime",
"tracks": "Tracks", "tracks": "Tracks",
"update": "Update", "update": "Update",
@ -408,8 +407,8 @@
"count": "{count} track|{count} track|{count} tracks", "count": "{count} track|{count} track|{count} tracks",
"edit": "Edit", "edit": "Edit",
"hide-previous": "Hide previous", "hide-previous": "Hide previous",
"save": "Save", "title": "Queue",
"title": "Queue" "save": "Save"
}, },
"radio": { "radio": {
"count": "{count} station|{count} station|{count} stations", "count": "{count} station|{count} station|{count} stations",
@ -495,7 +494,8 @@
"spotify": { "spotify": {
"no-support": "OwnTone was either built without support for Spotify or libspotify is not installed.", "no-support": "OwnTone was either built without support for Spotify or libspotify is not installed.",
"logged-as": "Logged in as ", "logged-as": "Logged in as ",
"requirements": "You must have a Spotify premium account. Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are: ", "requirements": "You must have a Spotify premium account.",
"scopes": "Access to the Spotify Web API enables scanning of your Spotify library. Required scopes are: ",
"user": "Access granted for ", "user": "Access granted for ",
"authorize": "Authorize Web API access", "authorize": "Authorize Web API access",
"grant-access": "Grant access to the Spotify Web API", "grant-access": "Grant access to the Spotify Web API",
@ -571,6 +571,10 @@
"toggle-lyrics": "Toggle lyrics" "toggle-lyrics": "Toggle lyrics"
} }
}, },
"setting": {
"not-saved": " (error saving setting)",
"saved": " (setting saved)"
},
"server": { "server": {
"connection-failed": "Failed to connect to OwnTone server", "connection-failed": "Failed to connect to OwnTone server",
"request-failed": "Request failed (status: {status} {cause} {url})", "request-failed": "Request failed (status: {status} {cause} {url})",

View File

@ -241,7 +241,6 @@
"compiled-with": "Compilé avec les options {options}.", "compiled-with": "Compilé avec les options {options}.",
"library": "Bibliothèque", "library": "Bibliothèque",
"more": "plus", "more": "plus",
"name": "Nom",
"total-playtime": "Durée totale de lecture", "total-playtime": "Durée totale de lecture",
"tracks": "Pistes", "tracks": "Pistes",
"update": "Actualiser", "update": "Actualiser",
@ -408,8 +407,8 @@
"count": "{count} piste|{count} piste|{count} pistes", "count": "{count} piste|{count} piste|{count} pistes",
"edit": "Éditer", "edit": "Éditer",
"hide-previous": "Masquer lhistorique", "hide-previous": "Masquer lhistorique",
"save": "Enregistrer", "queue": "File dattente",
"title": "File dattente" "save": "Enregistrer"
}, },
"radio": { "radio": {
"count": "{count} station|{count} station|{count} stations", "count": "{count} station|{count} station|{count} stations",
@ -495,7 +494,8 @@
"spotify": { "spotify": {
"no-support": "Loption Spotify nest pas présente.", "no-support": "Loption Spotify nest pas présente.",
"logged-as": "Connecté en tant que ", "logged-as": "Connecté en tant que ",
"requirements": "Vous devez posséder un compte Spotify Premium. Laccès à lAPI de Spotify permet lanalyse de votre bibliothèque Spotify. Les champs dapplication requis sont les suivants :", "requirements": "Vous devez posséder un compte Spotify Premium.",
"scopes": "Laccès à lAPI de Spotify permet lanalyse de votre bibliothèque Spotify. Les champs dapplication requis sont les suivants :",
"user": "Accès autorisé pour ", "user": "Accès autorisé pour ",
"authorize": "Autoriser laccès à lAPI", "authorize": "Autoriser laccès à lAPI",
"grant-access": "Accordez laccès à lAPI de Spotify", "grant-access": "Accordez laccès à lAPI de Spotify",
@ -571,6 +571,10 @@
"toggle-lyrics": "Voir/Cacher les paroles" "toggle-lyrics": "Voir/Cacher les paroles"
} }
}, },
"setting": {
"not-saved": " (erreur à lenregistrement du réglage)",
"saved": " (réglage enregistré)"
},
"server": { "server": {
"connection-failed": "Échec de connexion au serveur", "connection-failed": "Échec de connexion au serveur",
"request-failed": "La requête a échoué (status: {status} {cause} {url})", "request-failed": "La requête a échoué (status: {status} {cause} {url})",

View File

@ -241,7 +241,6 @@
"compiled-with": "编译支持来自于 {options}", "compiled-with": "编译支持来自于 {options}",
"library": "资料库", "library": "资料库",
"more": "更多", "more": "更多",
"name": "名称",
"total-playtime": "总播放时长", "total-playtime": "总播放时长",
"tracks": "曲目总数", "tracks": "曲目总数",
"update": "更新", "update": "更新",
@ -408,8 +407,8 @@
"count": "{count} 只曲目|{count} 只曲目", "count": "{count} 只曲目|{count} 只曲目",
"edit": "编辑", "edit": "编辑",
"hide-previous": "隐藏历史", "hide-previous": "隐藏历史",
"save": "保存", "title": "清单",
"title": "清单" "save": "保存"
}, },
"radio": { "radio": {
"count": "{count} 个电台|{count} 个电台", "count": "{count} 个电台|{count} 个电台",
@ -495,7 +494,8 @@
"spotify": { "spotify": {
"no-support": "OwnTone的构建没有来自 Spotify 官方的支持,也未安装 libspotify", "no-support": "OwnTone的构建没有来自 Spotify 官方的支持,也未安装 libspotify",
"logged-as": "登录为 ", "logged-as": "登录为 ",
"requirements": "您必须拥有 Spotify付费帐户。访问 Spotify Web API 可以扫描您的 Spotify库。所需范围是", "requirements": "您必须拥有 Spotify付费帐户",
"scopes": "访问 Spotify Web API 可以扫描您的 Spotify库。所需范围是",
"user": "授予访问权限", "user": "授予访问权限",
"authorize": "授权 Web API 访问", "authorize": "授权 Web API 访问",
"grant-access": "授予对 Spotify Web API 的访问权限", "grant-access": "授予对 Spotify Web API 的访问权限",
@ -571,6 +571,10 @@
"toggle-lyrics": "显示/隐藏歌词" "toggle-lyrics": "显示/隐藏歌词"
} }
}, },
"setting": {
"not-saved": " (设置保存错误)",
"saved": " (设置已保存)"
},
"server": { "server": {
"connection-failed": "无法连接到 OwnTone 服务器", "connection-failed": "无法连接到 OwnTone 服务器",
"request-failed": "请求失败 (状态:{status} {cause} {url})", "request-failed": "请求失败 (状态:{status} {cause} {url})",

View File

@ -241,7 +241,6 @@
"compiled-with": "編譯支持來自於 {options}", "compiled-with": "編譯支持來自於 {options}",
"library": "資料庫", "library": "資料庫",
"more": "更多", "more": "更多",
"name": "名稱",
"total-playtime": "總播放時長", "total-playtime": "總播放時長",
"tracks": "曲目總數", "tracks": "曲目總數",
"update": "更新", "update": "更新",
@ -408,8 +407,8 @@
"count": "{count} 首曲目|{count} 首曲目", "count": "{count} 首曲目|{count} 首曲目",
"edit": "編輯", "edit": "編輯",
"hide-previous": "隱藏歷史", "hide-previous": "隱藏歷史",
"save": "儲存", "title": "清單",
"title": "清單" "save": "儲存"
}, },
"radio": { "radio": {
"count": "{count} 個電台|{count} 個電台", "count": "{count} 個電台|{count} 個電台",
@ -495,7 +494,8 @@
"spotify": { "spotify": {
"no-support": "OwnTone並無 Spotify 官方的支持,也未安裝 libspotify", "no-support": "OwnTone並無 Spotify 官方的支持,也未安裝 libspotify",
"logged-as": "登入為 ", "logged-as": "登入為 ",
"requirements": "您必須擁有 Spotify付費帳戶。訪問 Spotify Web API 可以掃描您的 Spotify庫。所需範圍是", "requirements": "您必須擁有 Spotify付費帳戶",
"scopes": "訪問 Spotify Web API 可以掃描您的 Spotify庫。所需範圍是",
"user": "授予訪問權限", "user": "授予訪問權限",
"authorize": "授權 Web API 訪問", "authorize": "授權 Web API 訪問",
"grant-access": "授予對 Spotify Web API 的訪問權限", "grant-access": "授予對 Spotify Web API 的訪問權限",
@ -571,6 +571,10 @@
"toggle-lyrics": "顯示/隱藏歌詞" "toggle-lyrics": "顯示/隱藏歌詞"
} }
}, },
"setting": {
"not-saved": " (設定儲存錯誤)",
"saved": " (設定已儲存)"
},
"server": { "server": {
"connection-failed": "無法連接到 OwnTone 伺服器", "connection-failed": "無法連接到 OwnTone 伺服器",
"request-failed": "請求失敗 (狀態:{status} {cause} {url})", "request-failed": "請求失敗 (狀態:{status} {cause} {url})",

View File

@ -14,7 +14,6 @@ import {
mdiChevronDown, mdiChevronDown,
mdiChevronLeft, mdiChevronLeft,
mdiChevronUp, mdiChevronUp,
mdiClose,
mdiContentSave, mdiContentSave,
mdiDelete, mdiDelete,
mdiDeleteEmpty, mdiDeleteEmpty,
@ -80,7 +79,6 @@ export const icons = {
mdiChevronDown, mdiChevronDown,
mdiChevronLeft, mdiChevronLeft,
mdiChevronUp, mdiChevronUp,
mdiClose,
mdiContentSave, mdiContentSave,
mdiDelete, mdiDelete,
mdiDeleteEmpty, mdiDeleteEmpty,

View File

@ -1,12 +1,203 @@
@charset "utf-8"; @charset "utf-8";
@use 'bulma/bulma'; @import 'bulma/bulma.sass';
@use 'bulma/sass/utilities/mixins'; @import 'bulma-switch';
@media (prefers-color-scheme: dark) {
body,
html,
.content table th,
td,
.fd-tabs-section {
background-color: $black-ter !important;
color: $grey-light;
}
.tabs ul {
border-bottom-color: $grey-dark;
}
.tabs a:hover {
border-bottom-color: $grey-lighter;
color: $grey-lighter !important;
}
a:hover,
a.has-text-dark:hover,
a.has-text-dark:focus {
color: $grey-lighter !important;
}
.media + .media {
border-top-color: $grey-dark !important;
}
.tabs a {
border-bottom-color: $grey-dark;
}
.tabs a,
.hero.is-light .title,
.title,
.subtitle,
.navbar.is-light .navbar-brand > .navbar-item,
.navbar-item,
.navbar.is-white .navbar-brand a.navbar-item,
.navbar.is-dark .navbar-brand .navbar-item,
.navbar.is-light .navbar-burger {
color: $grey-light;
}
.navbar-item.has-dropdown-up .navbar-dropdown {
border-bottom-color: $grey-dark;
}
.navbar-dropdown {
background-color: $grey-darker;
border-top-color: $grey-dark;
}
a.tag:hover,
a.tag.is-delete:hover,
a.dropdown-item:hover,
a.dropdown-item:focus,
a.navbar-item:hover,
a.navbar-item:focus,
a.navbar-item:active,
.button:hover,
.button.is-white:focus,
.button.is-white:hover,
.button.is-dark:hover,
.button.is-light:hover,
hr,
.navbar-burger:hover,
.navbar.is-white .navbar-brand > a.navbar-item:hover,
.navbar.is-light .navbar-brand > a.navbar-item:focus,
.navbar.is-light .navbar-brand > a.navbar-item:hover,
.navbar.is-dark .navbar-brand > a.navbar-item:focus,
.navbar.is-dark .navbar-brand > a.navbar-item:hover,
.navbar-dropdown a.navbar-item:hover,
.navbar-dropdown a.navbar-item:focus,
.modal-content .input,
.modal-content select,
.tabs.is-toggle a:hover {
background-color: $grey-dark;
color: $grey-lighter;
}
.card-footer .has-text-dark,
.media .has-text-dark {
color: $grey-light !important;
}
.navbar-menu,
.navbar-brand,
.notification,
.card {
background-color: $grey-darker;
color: $grey-light;
}
a.tag,
.button,
.button:active,
.button:focus,
.dropdown-content,
.dropdown-item,
.input,
.input .switch {
background-color: $grey-darker;
border-width: 0;
color: $grey-lighter;
}
.input::placeholder,
.control.has-icons-left .icon {
color: $grey;
}
.label,
.tabs a:hover,
.control.has-icons-left .input:focus ~ .icon {
color: $grey-lighter;
}
.tabs.is-toggle a:hover,
.navbar-item .buttons .button,
.modal-content select,
.modal-content select:hover {
border-color: $grey-dark;
border-width: 1px;
}
.tabs.is-toggle a {
background-color: $grey-darker;
border-color: $grey-darker;
}
.button.is-light,
.button.is-dark,
.button.is-white,
.button[disabled],
.button[disabled]:hover {
background-color: $grey-darker;
color: $grey-light;
}
.has-text-grey-light,
a.has-text-grey-light:hover {
color: $grey !important;
}
.table,
code {
background: transparent;
}
.card-footer {
border-top-color: $grey-dark;
}
.card-footer-item:not(:last-child) {
border-right-color: $grey-dark;
}
.hero.is-light.is-bold {
background-image: linear-gradient(
141deg,
$black-ter 0%,
$grey-darker 71%,
$grey-dark
);
}
}
/* Lyrics animation */
@keyframes pop-color {
0% {
color: $black;
}
100% {
color: $success;
}
}
.media.with-progress h2:last-of-type {
margin-bottom: 6px;
}
.media.with-progress {
margin-top: 6px;
}
a.navbar-item {
padding: 0 1rem;
}
.fd-is-not-allowed {
cursor: not-allowed;
}
.fd-is-movable {
cursor: move;
}
.fd-is-square .button {
height: 27px;
min-width: 27px;
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.fd-is-text-clipped {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fd-tabs-section { .fd-tabs-section {
padding-bottom: 0; padding-bottom: 0;
padding-top: 0; padding-top: 0;
background: var(--bulma-body-background-color); background: $white;
top: $navbar-height;
z-index: 20; z-index: 20;
position: fixed; position: fixed;
width: 100%; width: 100%;
@ -14,12 +205,16 @@
.fd-has-shadow img { .fd-has-shadow img {
box-shadow: box-shadow:
0 0.25rem 0.5rem 0 var(--bulma-background-active), 0 4px 8px 0 rgba(0, 0, 0, 0.2),
0 0.375rem 1.25rem 0 var(--bulma-background-active); 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
.fd-page-with-tabs {
margin-top: $navbar-height !important;
} }
.is-full-height { .is-full-height {
min-height: calc(100vh - calc(2 * var(--bulma-navbar-height))); min-height: calc(100vh - calc(2 * $navbar-height));
} }
.is-disabled { .is-disabled {
@ -30,11 +225,6 @@
} }
} }
.navbar-item {
width: var(--bulma-navbar-height);
justify-content: center;
}
.fd-cover { .fd-cover {
align-items: center; align-items: center;
display: flex; display: flex;
@ -43,28 +233,28 @@
width: 4rem; width: 4rem;
height: 4rem; height: 4rem;
img { img {
border-radius: var(--bulma-radius-small); border-radius: $radius-small;
max-width: 4rem; max-width: 4rem;
max-height: 4rem; max-height: 4rem;
} }
} }
&-medium-image { &-medium-image {
@include mixins.tablet { @include from($tablet) {
justify-content: right; justify-content: right;
} }
img { img {
border-radius: var(--bulma-radius); border-radius: $radius;
max-height: calc(150px - 1.5rem); max-height: calc(150px - 1.5rem);
} }
} }
&-normal-image { &-normal-image {
img { img {
border-radius: var(--bulma-radius-large); border-radius: $radius-large;
width: 100%; width: 100%;
} }
} }
&-big-image { &-big-image {
@include mixins.mobile { @include mobile {
@media screen and (orientation: landscape) { @media screen and (orientation: landscape) {
img { img {
display: none; display: none;
@ -72,7 +262,7 @@
} }
} }
img { img {
border-radius: var(--bulma-radius-large); border-radius: $radius-large;
max-height: calc(100vh - 26rem); max-height: calc(100vh - 26rem);
} }
&.is-masked { &.is-masked {
@ -81,22 +271,69 @@
} }
} }
.sortable-chosen .media-right {
visibility: hidden;
}
.sortable-ghost h1,
.sortable-ghost h2 {
color: $danger;
}
.media:first-of-type { .media:first-of-type {
padding-top: 1rem; padding-top: 1rem;
} }
/* Transition effect */
.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;
}
/* Add a little bit of spacing between title and subtitle */
.title:not(.is-spaced) + .subtitle + .subtitle {
margin-top: -1.3rem !important;
}
/* Only scroll content if modal contains a card component */ /* Only scroll content if modal contains a card component */
.modal-content .card-content { .modal-content .card-content {
max-height: calc(100vh - calc(4 * var(--bulma-navbar-height))); max-height: calc(100vh - 200px);
overflow: auto; overflow: auto;
} }
.fd-width-auto {
min-width: auto;
}
/* Show scrollbar for navbar menu in desktop mode if content exceeds the screen size */
@include desktop {
.navbar-dropdown {
max-height: calc(100vh - calc(2 * $navbar-height) - 2rem);
overflow: auto;
}
}
/* Limit the size of the bottom navbar menu to not be displayed behind the Safari browser menu on iOS */
.fd-bottom-navbar .navbar-menu {
max-height: calc(100vh - calc(2 * $navbar-height) - 1rem);
overflow: scroll;
}
.buttons { .buttons {
@include mixins.mobile { @include mobile {
&.is-centered-mobile { &.fd-is-centered-mobile {
justify-content: center; justify-content: center;
&:not(.has-addons) { &:not(.has-addons) {
.button { .button:not(.is-fullwidth) {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
} }
@ -107,16 +344,16 @@
.column { .column {
&.fd-has-cover { &.fd-has-cover {
@include mixins.mobile { @include mobile {
margin: auto; margin: auto;
} }
@include mixins.tablet { @include from($tablet) {
margin-right: 0; margin-right: 0;
} }
} }
} }
.overlay-fullscreen { .fd-overlay-fullscreen {
@extend .is-overlay; @extend .is-overlay;
z-index: 25; z-index: 25;
background-color: rgba(10, 10, 10, 0.2); background-color: rgba(10, 10, 10, 0.2);
@ -127,13 +364,100 @@
padding: 1.5rem !important; padding: 1.5rem !important;
} }
.dropdown-menu { /* Slider */
@include mixins.mobile { @mixin thumb {
width: 100vw; -webkit-appearance: none;
width: var(--th);
height: var(--th);
box-sizing: border-box;
border-radius: 50%;
background: $light;
border: 1px solid $grey-light;
@media (prefers-color-scheme: dark) {
background: $grey-light;
border: 1px solid $grey-dark;
} }
} }
.dropdown-content { @mixin thumb-inactive {
max-height: calc(100vh - calc(2 * var(--bulma-navbar-height))); box-sizing: border-box;
overflow: auto; background-color: $light;
@media (prefers-color-scheme: dark) {
background-color: $grey-dark;
border: 1px solid $grey-darker;
}
}
@mixin track {
height: calc(var(--sh));
border-radius: calc(var(--sh) / 2);
background: linear-gradient(90deg, $dark var(--sx), $grey-light var(--sx));
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
$grey-light var(--sx),
$grey-dark var(--sx)
);
}
}
@mixin track-inactive {
background: linear-gradient(90deg, $grey-light var(--sx), $light var(--sx));
@media (prefers-color-scheme: dark) {
background: linear-gradient(
90deg,
$grey-dark var(--sx),
$black-ter var(--sx)
);
}
}
input[type='range'].slider {
--sh: 0.25rem;
--th: calc(var(--sh) * 4);
background-color: transparent;
@include 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;
}
}
} }

View File

@ -1,166 +1,180 @@
<template> <template>
<section class="section"> <div>
<div class="container"> <section class="section">
<div class="columns is-centered"> <div class="container">
<div class="column is-four-fifths"> <div class="columns is-centered">
<div class="content"> <div class="column is-four-fifths has-text-centered-mobile">
<nav class="level"> <h1 class="title is-4" v-text="configuration.library_name" />
<div class="level-left"> </div>
<div class="level-item"> </div>
<p class="title is-4" v-text="$t('page.about.library')" /> </div>
</section>
<section class="section">
<div class="container">
<div class="columns is-centered">
<div class="column is-four-fifths">
<div class="content">
<nav class="level is-mobile">
<!-- Left side -->
<div class="level-left">
<div class="level-item">
<p class="title is-5" v-text="$t('page.about.library')" />
</div>
</div> </div>
</div> <!-- Right side -->
<div class="level-right"> <div class="level-right">
<div v-if="library.updating"> <div v-if="library.updating">
<a <a
class="button is-small is-loading" class="button is-small is-loading"
v-text="$t('page.about.update')" v-text="$t('page.about.update')"
/> />
</div>
<div v-else>
<a
class="button is-small"
@click="showUpdateDialog()"
v-text="$t('page.about.update')"
/>
</div>
</div> </div>
<div v-else> </nav>
<a <table class="table">
class="button is-small" <tbody>
@click="showUpdateDialog()" <tr>
v-text="$t('page.about.update')" <th
/> class="has-text-left"
</div> v-text="$t('page.about.artists')"
</div> />
</nav> <td
<div class="media"> class="has-text-right"
<div v-text="$filters.number(library.artists)"
class="media-content has-text-weight-bold" />
v-text="$t('page.about.name')" </tr>
/> <tr>
<div class="media-right" v-text="configuration.library_name" /> <th
</div> class="has-text-left"
<div class="media"> v-text="$t('page.about.albums')"
<div />
class="media-content has-text-weight-bold" <td
v-text="$t('page.about.artists')" class="has-text-right"
/> v-text="$filters.number(library.albums)"
<div />
class="media-right" </tr>
v-text="$filters.number(library.artists)" <tr>
/> <th
</div> class="has-text-left"
<div class="media"> v-text="$t('page.about.tracks')"
<div />
class="media-content has-text-weight-bold" <td
v-text="$t('page.about.albums')" class="has-text-right"
/> v-text="$filters.number(library.songs)"
<div />
media="media-right" </tr>
v-text="$filters.number(library.albums)" <tr>
/> <th
</div> class="has-text-left"
<div class="media"> v-text="$t('page.about.total-playtime')"
<div />
class="media-content has-text-weight-bold" <td
v-text="$t('page.about.tracks')" class="has-text-right"
/> v-text="
<div $filters.durationInDays(library.db_playtime * 1000)
class="media-right" "
v-text="$filters.number(library.songs)" />
/> </tr>
</div> <tr>
<div class="media"> <th
<div class="has-text-left"
class="media-content has-text-weight-bold" v-text="$t('page.about.updated')"
v-text="$t('page.about.total-playtime')" />
/> <td class="has-text-right">
<div <span
class="media-right" v-text="
v-text="$filters.durationInDays(library.db_playtime)" $t('page.about.updated-on', {
/> time: $filters.timeFromNow(library.updated_at)
</div> })
<div class="media"> "
<div />
class="media-content has-text-weight-bold" (<span
v-text="$t('page.about.updated')" class="has-text-grey"
/> v-text="$filters.datetime(library.updated_at)"
<div class="media-right"> />)
<span </td>
v-text=" </tr>
$t('page.about.updated-on', { <tr>
time: $filters.timeFromNow(library.updated_at) <th
}) class="has-text-left"
" v-text="$t('page.about.uptime')"
/> />
(<span <td class="has-text-right">
class="has-text-grey" <span
v-text="$filters.datetime(library.updated_at)" v-text="$filters.timeFromNow(library.started_at, true)"
/>) />
</div> (<span
</div> class="has-text-grey"
<div class="media"> v-text="$filters.datetime(library.started_at)"
<div />)
class="media-content has-text-weight-bold" </td>
v-text="$t('page.about.uptime')" </tr>
/> </tbody>
<div class="media-right"> </table>
<span v-text="$filters.timeFromNow(library.started_at, true)" />
(<span
class="has-text-grey"
v-text="$filters.datetime(library.started_at)"
/>)
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</section> <section class="section">
<section class="section"> <div class="container">
<div class="container"> <div class="columns is-centered">
<div class="columns is-centered"> <div class="column is-four-fifths">
<div class="column is-four-fifths"> <div class="content has-text-centered-mobile">
<div class="content has-text-centered-mobile"> <p
<p class="is-size-7"
class="is-size-7" v-text="
v-text=" $t('page.about.version', { version: configuration.version })
$t('page.about.version', { version: configuration.version }) "
" />
/> <p
<p class="is-size-7"
class="is-size-7" v-text="
v-text=" $t('page.about.compiled-with', {
$t('page.about.compiled-with', { options: configuration.buildoptions.join(', ')
options: configuration.buildoptions.join(', ') })
}) "
" />
/> <i18n-t
<i18n-t tag="p"
tag="p" class="is-size-7"
class="is-size-7" keypath="page.about.built-with"
keypath="page.about.built-with" scope="global"
scope="global" >
> <template #bulma>
<template #bulma> <a href="https://bulma.io">Bulma</a>
<a href="https://bulma.io">Bulma</a> </template>
</template> <template #mdi>
<template #mdi> <a href="https://pictogrammers.com/library/mdi/">
<a href="https://pictogrammers.com/library/mdi/"> Material Design Icons
Material Design Icons </a>
</a> </template>
</template> <template #vuejs>
<template #vuejs> <a href="https://vuejs.org/">Vue.js</a>
<a href="https://vuejs.org/">Vue.js</a> </template>
</template> <template #axios>
<template #axios> <a href="https://github.com/axios/axios">axios</a>
<a href="https://github.com/axios/axios">axios</a> </template>
</template> <template #others>
<template #others> <a
<a href="https://github.com/owntone/owntone-server/network/dependencies"
href="https://github.com/owntone/owntone-server/network/dependencies" v-text="$t('page.about.more')"
v-text="$t('page.about.more')" />
/> </template>
</template> </i18n-t>
</i18n-t> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
</section> </div>
</template> </template>
<script> <script>
@ -195,3 +209,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -2,20 +2,17 @@
<div> <div>
<content-with-hero> <content-with-hero>
<template #heading-left> <template #heading-left>
<div class="title is-5" v-text="album.name" /> <h1 class="title is-5" v-text="album.name" />
<div class="subtitle is-6"> <h2 class="subtitle is-6 has-text-link">
<a @click="open_artist" v-text="album.artist" /> <a class="has-text-link" @click="open_artist" v-text="album.artist" />
</div> </h2>
<div class="buttons is-centered-mobile mt-5"> <div class="buttons fd-is-centered-mobile mt-5">
<a <a class="button is-small is-dark is-rounded" @click="play">
class="button has-background-light is-small is-rounded"
@click="play"
>
<mdicon class="icon" name="shuffle" size="16" /> <mdicon class="icon" name="shuffle" size="16" />
<span v-text="$t('page.album.shuffle')" /> <span v-text="$t('page.album.shuffle')" />
</a> </a>
<a <a
class="button is-small has-background-light is-rounded" class="button is-small is-light is-rounded"
@click="show_details_modal = true" @click="show_details_modal = true"
> >
<mdicon class="icon" name="dots-horizontal" size="16" /> <mdicon class="icon" name="dots-horizontal" size="16" />
@ -32,8 +29,8 @@
/> />
</template> </template>
<template #content> <template #content>
<div <p
class="is-size-7 is-uppercase has-text-centered-mobile my-5" class="heading has-text-centered-mobile mt-5"
v-text="$t('page.album.track-count', { count: album.track_count })" v-text="$t('page.album.track-count', { count: album.track_count })"
/> />
<list-tracks :items="tracks" :uris="album.uri" /> <list-tracks :items="tracks" :uris="album.uri" />
@ -107,3 +104,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -2,11 +2,15 @@
<div> <div>
<content-with-hero> <content-with-hero>
<template #heading-left> <template #heading-left>
<div class="title is-5" v-text="album.name" /> <h1 class="title is-5" v-text="album.name" />
<div class="subtitle is-6"> <h2 class="subtitle is-6 has-text-link">
<a @click="open_artist" v-text="album.artists[0].name" /> <a
</div> class="has-text-link"
<div class="buttons is-centered-mobile mt-5"> @click="open_artist"
v-text="album.artists[0].name"
/>
</h2>
<div class="buttons fd-is-centered-mobile mt-5">
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<mdicon class="icon" name="shuffle" size="16" /> <mdicon class="icon" name="shuffle" size="16" />
<span v-text="$t('page.spotify.album.shuffle')" /> <span v-text="$t('page.spotify.album.shuffle')" />
@ -29,8 +33,8 @@
/> />
</template> </template>
<template #content> <template #content>
<div <p
class="is-size-7 is-uppercase has-text-centered-mobile mt-5" class="heading has-text-centered-mobile mt-5"
v-text=" v-text="
$t('page.spotify.album.track-count', { count: album.tracks.total }) $t('page.spotify.album.track-count', { count: album.tracks.total })
" "
@ -123,3 +127,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,54 +1,60 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
<index-button-list :indices="albums.indices" /> <index-button-list :indices="albums.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.albums.filter')" />
class="is-size-7 is-uppercase" <div class="field">
v-text="$t('page.albums.filter')" <div class="control">
/> <input
<control-switch v-model="uiStore.hide_singles"> id="switchHideSingles"
<template #label> v-model="hide_singles"
<span v-text="$t('page.albums.hide-singles')" /> type="checkbox"
</template> class="switch is-rounded"
<template #help> />
<span v-text="$t('page.albums.hide-singles-help')" /> <label
</template> for="switchHideSingles"
</control-switch> v-text="$t('page.albums.hide-singles')"
<control-switch />
v-if="spotify_enabled" </div>
v-model="uiStore.hide_spotify" <p class="help" v-text="$t('page.albums.hide-singles-help')" />
> </div>
<template #label> <div v-if="spotify_enabled" class="field">
<span v-text="$t('page.albums.hide-spotify')" /> <div class="control">
</template> <input
<template #help> id="switchHideSpotify"
<span v-text="$t('page.albums.hide-spotify-help')" /> v-model="hide_spotify"
</template> type="checkbox"
</control-switch> class="switch is-rounded"
/>
<label
for="switchHideSpotify"
v-text="$t('page.albums.hide-spotify')"
/>
</div>
<p class="help" v-text="$t('page.albums.hide-spotify-help')" />
</div>
</div> </div>
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.albums.sort.title')" />
class="is-size-7 is-uppercase"
v-text="$t('page.albums.sort.title')"
/>
<control-dropdown <control-dropdown
v-model:value="uiStore.albums_sort" v-model:value="selected_grouping_id"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="$t('page.albums.title')" /> <p class="title is-4" v-text="$t('page.albums.title')" />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.albums.count', { count: albums.count })" v-text="$t('page.albums.count', { count: albums.count })"
/> />
</template> </template>
<template #heading-right />
<template #content> <template #content>
<list-albums :items="albums" /> <list-albums :items="albums" />
</template> </template>
@ -59,7 +65,6 @@
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue' import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList' import { GroupedList } from '@/lib/GroupedList'
import IndexButtonList from '@/components/IndexButtonList.vue' import IndexButtonList from '@/components/IndexButtonList.vue'
import ListAlbums from '@/components/ListAlbums.vue' import ListAlbums from '@/components/ListAlbums.vue'
@ -83,7 +88,6 @@ export default {
components: { components: {
ContentWithHeading, ContentWithHeading,
ControlDropdown, ControlDropdown,
ControlSwitch,
IndexButtonList, IndexButtonList,
ListAlbums, ListAlbums,
TabsMusic TabsMusic
@ -153,17 +157,43 @@ export default {
computed: { computed: {
albums() { albums() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.albums_sort (grouping) => grouping.id === this.selected_grouping_id
) )
options.filters = [ options.filters = [
(album) => !this.uiStore.hide_singles || album.track_count > 2, (album) => !this.hide_singles || album.track_count > 2,
(album) => !this.uiStore.hide_spotify || album.data_kind !== 'spotify' (album) => !this.hide_spotify || album.data_kind !== 'spotify'
] ]
return this.albums_list.group(options) return this.albums_list.group(options)
}, },
hide_singles: {
get() {
return this.uiStore.hide_singles
},
set(value) {
this.uiStore.hide_singles = value
}
},
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.albums_sort
},
set(value) {
this.uiStore.albums_sort = value
}
},
spotify_enabled() { spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid return this.servicesStore.spotify.webapi_token_valid
} }
} }
} }
</script> </script>
<style></style>

View File

@ -4,46 +4,34 @@
<template #options> <template #options>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.artist.filter')" />
class="is-size-7 is-uppercase" <div v-if="spotify_enabled" class="field">
v-text="$t('page.artist.filter')" <div class="control">
/> <input
<control-switch id="switchHideSpotify"
v-if="spotify_enabled" v-model="hide_spotify"
v-model="uiStore.hide_spotify" type="checkbox"
> class="switch is-rounded"
<template #label> />
<span v-text="$t('page.artist.hide-spotify')" /> <label
</template> for="switchHideSpotify"
<template #help> v-text="$t('page.artist.hide-spotify')"
<span v-text="$t('page.artist.hide-spotify-help')" /> />
</template> </div>
</control-switch> <p class="help" v-text="$t('page.artist.hide-spotify-help')" />
</div>
</div> </div>
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.artist.sort.title')" />
class="is-size-7 is-uppercase"
v-text="$t('page.artist.sort.title')"
/>
<control-dropdown <control-dropdown
v-model:value="uiStore.artist_albums_sort" v-model:value="selected_grouping_id"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="artist.name" /> <p class="title is-4" v-text="artist.name" />
<div class="is-size-7 is-uppercase">
<span
v-text="$t('page.artist.album-count', { count: albums.count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
@click="open_tracks"
v-text="$t('page.artist.track-count', { count: track_count })"
/>
</div>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -60,6 +48,17 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p class="heading has-text-centered-mobile">
<span
v-text="$t('page.artist.album-count', { count: albums.count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
class="has-text-link"
@click="open_tracks"
v-text="$t('page.artist.track-count', { count: track_count })"
/>
</p>
<list-albums :items="albums" /> <list-albums :items="albums" />
<modal-dialog-artist <modal-dialog-artist
:item="artist" :item="artist"
@ -74,7 +73,6 @@
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue' import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList' import { GroupedList } from '@/lib/GroupedList'
import ListAlbums from '@/components/ListAlbums.vue' import ListAlbums from '@/components/ListAlbums.vue'
import ModalDialogArtist from '@/components/ModalDialogArtist.vue' import ModalDialogArtist from '@/components/ModalDialogArtist.vue'
@ -101,7 +99,6 @@ export default {
components: { components: {
ContentWithHeading, ContentWithHeading,
ControlDropdown, ControlDropdown,
ControlSwitch,
ListAlbums, ListAlbums,
ModalDialogArtist ModalDialogArtist
}, },
@ -139,13 +136,29 @@ export default {
computed: { computed: {
albums() { albums() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.artist_albums_sort (grouping) => grouping.id === this.selected_grouping_id
) )
options.filters = [ options.filters = [
(album) => !this.uiStore.hide_spotify || album.data_kind !== 'spotify' (album) => !this.hide_spotify || album.data_kind !== 'spotify'
] ]
return this.albums_list.group(options) return this.albums_list.group(options)
}, },
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.artist_albums_sort
},
set(value) {
this.uiStore.artist_albums_sort = value
}
},
spotify_enabled() { spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid return this.servicesStore.spotify.webapi_token_valid
}, },
@ -174,3 +187,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -2,11 +2,7 @@
<div> <div>
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="artist.name" /> <p class="title is-4" v-text="artist.name" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.spotify.artist.album-count', { count: total })"
/>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -23,6 +19,10 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p
class="heading has-text-centered-mobile"
v-text="$t('page.spotify.artist.album-count', { count: total })"
/>
<list-albums-spotify :items="albums" /> <list-albums-spotify :items="albums" />
<VueEternalLoading v-if="offset < total" :load="load_next"> <VueEternalLoading v-if="offset < total" :load="load_next">
<template #loading> <template #loading>
@ -140,3 +140,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -5,29 +5,27 @@
<index-button-list :indices="tracks.indices" /> <index-button-list :indices="tracks.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<p <p class="heading mb-5" v-text="$t('page.artist.filter')" />
class="is-size-7 is-uppercase" <div v-if="spotify_enabled" class="field">
v-text="$t('page.artist.filter')" <div class="control">
/> <input
<control-switch id="switchHideSpotify"
v-if="spotify_enabled" v-model="hide_spotify"
v-model="uiStore.hide_spotify" type="checkbox"
> class="switch is-rounded"
<template #label> />
<span v-text="$t('page.artist.hide-spotify')" /> <label
</template> for="switchHideSpotify"
<template #help> v-text="$t('page.artist.hide-spotify')"
<span v-text="$t('page.artist.hide-spotify-help')" /> />
</template> </div>
</control-switch> <p class="help" v-text="$t('page.artist.hide-spotify-help')" />
</div>
</div> </div>
<div class="column"> <div class="column">
<p <p class="heading mb-5" v-text="$t('page.artist.sort.title')" />
class="is-size-7 is-uppercase"
v-text="$t('page.artist.sort.title')"
/>
<control-dropdown <control-dropdown
v-model:value="uiStore.artist_tracks_sort" v-model:value="selected_grouping_id"
:options="groupings" :options="groupings"
/> />
</div> </div>
@ -35,16 +33,6 @@
</template> </template>
<template #heading-left> <template #heading-left>
<p class="title is-4" v-text="artist.name" /> <p class="title is-4" v-text="artist.name" />
<div class="is-size-7 is-uppercase">
<a
@click="open_artist"
v-text="$t('page.artist.album-count', { count: album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.artist.track-count', { count: tracks.count })"
/>
</div>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -61,6 +49,17 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p class="heading has-text-centered-mobile">
<a
class="has-text-link"
@click="open_artist"
v-text="$t('page.artist.album-count', { count: album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.artist.track-count', { count: tracks.count })"
/>
</p>
<list-tracks :items="tracks" :uris="track_uris" /> <list-tracks :items="tracks" :uris="track_uris" />
<modal-dialog-artist <modal-dialog-artist
:item="artist" :item="artist"
@ -75,7 +74,6 @@
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue' import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList' import { GroupedList } from '@/lib/GroupedList'
import IndexButtonList from '@/components/IndexButtonList.vue' import IndexButtonList from '@/components/IndexButtonList.vue'
import ListTracks from '@/components/ListTracks.vue' import ListTracks from '@/components/ListTracks.vue'
@ -103,7 +101,6 @@ export default {
components: { components: {
ContentWithHeading, ContentWithHeading,
ControlDropdown, ControlDropdown,
ControlSwitch,
IndexButtonList, IndexButtonList,
ListTracks, ListTracks,
ModalDialogArtist ModalDialogArtist
@ -150,6 +147,22 @@ export default {
.map((track) => track.item.album_id) .map((track) => track.item.album_id)
).size ).size
}, },
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.artist_tracks_sort
},
set(value) {
this.uiStore.artist_tracks_sort = value
}
},
spotify_enabled() { spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid return this.servicesStore.spotify.webapi_token_valid
}, },
@ -158,10 +171,10 @@ export default {
}, },
tracks() { tracks() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.artist_tracks_sort (grouping) => grouping.id === this.selected_grouping_id
) )
options.filters = [ options.filters = [
(track) => !this.uiStore.hide_spotify || track.data_kind !== 'spotify' (track) => !this.hide_spotify || track.data_kind !== 'spotify'
] ]
return this.tracks_list.group(options) return this.tracks_list.group(options)
} }
@ -184,3 +197,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,53 +1,60 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
<index-button-list :indices="artists.indices" /> <index-button-list :indices="artists.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.artists.filter')" />
class="is-size-7 is-uppercase" <div class="field">
v-text="$t('page.artists.filter')" <div class="control">
/> <input
<control-switch v-model="uiStore.hide_singles"> id="switchHideSingles"
<template #label> v-model="hide_singles"
<span v-text="$t('page.artists.hide-singles')" /> type="checkbox"
</template> class="switch is-rounded"
<template #help> />
<span v-text="$t('page.artists.hide-singles-help')" /> <label
</template> for="switchHideSingles"
</control-switch> v-text="$t('page.artists.hide-singles')"
/>
</div>
<p class="help" v-text="$t('page.artists.hide-singles-help')" />
</div>
<div v-if="spotify_enabled" class="field"> <div v-if="spotify_enabled" class="field">
<control-switch v-model="uiStore.hide_spotify"> <div class="control">
<template #label> <input
<span v-text="$t('page.artists.hide-spotify')" /> id="switchHideSpotify"
</template> v-model="hide_spotify"
<template #help> type="checkbox"
<span v-text="$t('page.artists.hide-spotify-help')" /> class="switch is-rounded"
</template> />
</control-switch> <label
for="switchHideSpotify"
v-text="$t('page.artists.hide-spotify')"
/>
</div>
<p class="help" v-text="$t('page.artists.hide-spotify-help')" />
</div> </div>
</div> </div>
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.artists.sort.title')" />
class="is-size-7 is-uppercase"
v-text="$t('page.artists.sort.title')"
/>
<control-dropdown <control-dropdown
v-model:value="uiStore.artists_sort" v-model:value="selected_grouping_id"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="$t('page.artists.title')" /> <p class="title is-4" v-text="$t('page.artists.title')" />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.artists.count', { count: artists.count })" v-text="$t('page.artists.count', { count: artists.count })"
/> />
</template> </template>
<template #heading-right />
<template #content> <template #content>
<list-artists :items="artists" /> <list-artists :items="artists" />
</template> </template>
@ -58,7 +65,6 @@
<script> <script>
import ContentWithHeading from '@/templates/ContentWithHeading.vue' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ControlDropdown from '@/components/ControlDropdown.vue' import ControlDropdown from '@/components/ControlDropdown.vue'
import ControlSwitch from '@/components/ControlSwitch.vue'
import { GroupedList } from '@/lib/GroupedList' import { GroupedList } from '@/lib/GroupedList'
import IndexButtonList from '@/components/IndexButtonList.vue' import IndexButtonList from '@/components/IndexButtonList.vue'
import ListArtists from '@/components/ListArtists.vue' import ListArtists from '@/components/ListArtists.vue'
@ -82,7 +88,6 @@ export default {
components: { components: {
ContentWithHeading, ContentWithHeading,
ControlDropdown, ControlDropdown,
ControlSwitch,
IndexButtonList, IndexButtonList,
ListArtists, ListArtists,
TabsMusic TabsMusic
@ -120,21 +125,47 @@ export default {
}, },
computed: { computed: {
// Wraps GroupedList and updates it if filter or sort changes
artists() { artists() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.artists_sort (grouping) => grouping.id === this.selected_grouping_id
) )
options.filters = [ options.filters = [
(artist) => (artist) =>
!this.uiStore.hide_singles || !this.hide_singles || artist.track_count > artist.album_count * 2,
artist.track_count > artist.album_count * 2, (artist) => !this.hide_spotify || artist.data_kind !== 'spotify'
(artist) => !this.uiStore.hide_spotify || artist.data_kind !== 'spotify'
] ]
return this.artists_list.group(options) return this.artists_list.group(options)
}, },
hide_singles: {
get() {
return this.uiStore.hide_singles
},
set(value) {
this.uiStore.hide_singles = value
}
},
hide_spotify: {
get() {
return this.uiStore.hide_spotify
},
set(value) {
this.uiStore.hide_spotify = value
}
},
selected_grouping_id: {
get() {
return this.uiStore.artists_sort
},
set(value) {
this.uiStore.artists_sort = value
}
},
spotify_enabled() { spotify_enabled() {
return this.servicesStore.spotify.webapi_token_valid return this.servicesStore.spotify.webapi_token_valid
} }
} }
} }
</script> </script>
<style></style>

View File

@ -2,11 +2,11 @@
<div> <div>
<content-with-hero> <content-with-hero>
<template #heading-left> <template #heading-left>
<div class="title is-5" v-text="album.name" /> <h1 class="title is-5" v-text="album.name" />
<div class="subtitle is-6"> <h2 class="subtitle is-6 has-text-link">
<a @click="open_artist" v-text="album.artist" /> <a class="has-text-link" @click="open_artist" v-text="album.artist" />
</div> </h2>
<div class="buttons is-centered-mobile mt-5"> <div class="buttons fd-is-centered-mobile mt-5">
<a class="button is-small is-dark is-rounded" @click="play"> <a class="button is-small is-dark is-rounded" @click="play">
<mdicon class="icon" name="play" size="16" /> <mdicon class="icon" name="play" size="16" />
<span v-text="$t('page.audiobooks.album.play')" /> <span v-text="$t('page.audiobooks.album.play')" />
@ -29,8 +29,8 @@
/> />
</template> </template>
<template #content> <template #content>
<div <p
class="is-size-7 is-uppercase has-text-centered-mobile mt-5" class="heading has-text-centered-mobile mt-5"
v-text=" v-text="
$t('page.audiobooks.album.track-count', { $t('page.audiobooks.album.track-count', {
count: album.track_count count: album.track_count
@ -103,3 +103,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-audiobooks /> <tabs-audiobooks />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
@ -8,7 +8,7 @@
<template #heading-left> <template #heading-left>
<p class="title is-4" v-text="$t('page.audiobooks.albums.title')" /> <p class="title is-4" v-text="$t('page.audiobooks.albums.title')" />
<p <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.audiobooks.albums.count', { count: albums.count })" v-text="$t('page.audiobooks.albums.count', { count: albums.count })"
/> />
</template> </template>
@ -61,3 +61,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -2,15 +2,7 @@
<div> <div>
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="artist.name" /> <p class="title is-4" v-text="artist.name" />
<div
class="is-size-7 is-uppercase"
v-text="
$t('page.audiobooks.artist.album-count', {
count: artist.album_count
})
"
/>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -27,6 +19,14 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p
class="heading has-text-centered-mobile"
v-text="
$t('page.audiobooks.artist.album-count', {
count: artist.album_count
})
"
/>
<list-albums :items="albums" /> <list-albums :items="albums" />
<modal-dialog-artist <modal-dialog-artist
:item="artist" :item="artist"
@ -87,3 +87,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,17 +1,18 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-audiobooks /> <tabs-audiobooks />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
<index-button-list :indices="artists.indices" /> <index-button-list :indices="artists.indices" />
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="$t('page.audiobooks.artists.title')" /> <p class="title is-4" v-text="$t('page.audiobooks.artists.title')" />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.audiobooks.artists.count', { count: artists.count })" v-text="$t('page.audiobooks.artists.count', { count: artists.count })"
/> />
</template> </template>
<template #heading-right />
<template #content> <template #content>
<list-artists :items="artists" /> <list-artists :items="artists" />
</template> </template>
@ -61,3 +62,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,14 +1,14 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-audiobooks /> <tabs-audiobooks />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
<index-button-list :indices="genres.indices" /> <index-button-list :indices="genres.indices" />
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="$t('page.genres.title')" /> <p class="title is-4" v-text="$t('page.genres.title')" />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.genres.count', { count: genres.total })" v-text="$t('page.genres.count', { count: genres.total })"
/> />
</template> </template>
@ -61,3 +61,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -2,21 +2,7 @@
<div> <div>
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="composer.name" /> <p class="title is-4" v-text="composer.name" />
<div class="is-size-7 is-uppercase">
<span
v-text="
$t('page.composer.album-count', { count: composer.album_count })
"
/>
<span>&nbsp;|&nbsp;</span>
<a
@click="open_tracks"
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</div>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -33,6 +19,21 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p class="heading has-text-centered-mobile">
<span
v-text="
$t('page.composer.album-count', { count: composer.album_count })
"
/>
<span>&nbsp;|&nbsp;</span>
<a
class="has-text-link"
@click="open_tracks"
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</p>
<list-albums :items="albums" /> <list-albums :items="albums" />
<modal-dialog-composer <modal-dialog-composer
:item="composer" :item="composer"
@ -103,3 +104,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -5,35 +5,16 @@
<index-button-list :indices="tracks.indices" /> <index-button-list :indices="tracks.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.artist.sort.title')" />
class="is-size-7 is-uppercase"
v-text="$t('page.artist.sort.title')"
/>
<control-dropdown <control-dropdown
v-model:value="uiStore.composer_tracks_sort" v-model:value="selected_grouping_id"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="composer.name" /> <p class="title is-4" v-text="composer.name" />
<div class="is-size-7 is-uppercase">
<a
@click="open_albums"
v-text="
$t('page.composer.album-count', {
count: composer.album_count
})
"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</div>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -50,6 +31,23 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p class="heading has-text-centered-mobile">
<a
class="has-text-link"
@click="open_albums"
v-text="
$t('page.composer.album-count', {
count: composer.album_count
})
"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="
$t('page.composer.track-count', { count: composer.track_count })
"
/>
</p>
<list-tracks :items="tracks" :expression="expression" /> <list-tracks :items="tracks" :expression="expression" />
<modal-dialog-composer <modal-dialog-composer
:item="composer" :item="composer"
@ -132,9 +130,17 @@ export default {
expression() { expression() {
return `composer is "${this.composer.name}" and media_kind is music` return `composer is "${this.composer.name}" and media_kind is music`
}, },
selected_grouping_id: {
get() {
return this.uiStore.composer_tracks_sort
},
set(value) {
this.uiStore.composer_tracks_sort = value
}
},
tracks() { tracks() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.composer_tracks_sort (grouping) => grouping.id === this.selected_grouping_id
) )
return this.tracks_list.group(options) return this.tracks_list.group(options)
} }
@ -154,3 +160,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,14 +1,14 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
<index-button-list :indices="composers.indices" /> <index-button-list :indices="composers.indices" />
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="$t('page.composers.title')" /> <p class="title is-4" v-text="$t('page.composers.title')" />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.composers.count', { count: composers.total })" v-text="$t('page.composers.count', { count: composers.total })"
/> />
</template> </template>
@ -56,3 +56,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

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

View File

@ -5,17 +5,7 @@
<index-button-list :indices="albums.indices" /> <index-button-list :indices="albums.indices" />
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="genre.name" /> <p class="title is-4" v-text="genre.name" />
<div class="is-size-7 is-uppercase">
<span
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
@click="open_tracks"
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</div>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -32,6 +22,17 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p class="heading has-text-centered-mobile">
<span
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<a
class="has-text-link"
@click="open_tracks"
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</p>
<list-albums :items="albums" /> <list-albums :items="albums" />
<modal-dialog-genre <modal-dialog-genre
:item="genre" :item="genre"
@ -108,3 +109,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -5,29 +5,16 @@
<index-button-list :indices="tracks.indices" /> <index-button-list :indices="tracks.indices" />
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<div <p class="heading mb-5" v-text="$t('page.genre.sort.title')" />
class="is-size-7 is-uppercase"
v-text="$t('page.genre.sort.title')"
/>
<control-dropdown <control-dropdown
v-model:value="uiStore.genre_tracks_sort" v-model:value="selected_grouping_id"
:options="groupings" :options="groupings"
/> />
</div> </div>
</div> </div>
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="genre.name" /> <p class="title is-4" v-text="genre.name" />
<div class="is-size-7 is-uppercase">
<a
@click="open_genre"
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</div>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -44,6 +31,17 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p class="heading has-text-centered-mobile">
<a
class="has-text-link"
@click="open_genre"
v-text="$t('page.genre.album-count', { count: genre.album_count })"
/>
<span>&nbsp;|&nbsp;</span>
<span
v-text="$t('page.genre.track-count', { count: genre.track_count })"
/>
</p>
<list-tracks :items="tracks" :expression="expression" /> <list-tracks :items="tracks" :expression="expression" />
<modal-dialog-genre <modal-dialog-genre
:item="genre" :item="genre"
@ -128,9 +126,17 @@ export default {
expression() { expression() {
return `genre is "${this.genre.name}" and media_kind is ${this.media_kind}` return `genre is "${this.genre.name}" and media_kind is ${this.media_kind}`
}, },
selected_grouping_id: {
get() {
return this.uiStore.genre_tracks_sort
},
set(value) {
this.uiStore.genre_tracks_sort = value
}
},
tracks() { tracks() {
const { options } = this.groupings.find( const { options } = this.groupings.find(
(grouping) => grouping.id === this.uiStore.genre_tracks_sort (grouping) => grouping.id === this.selected_grouping_id
) )
return this.tracks_list.group(options) return this.tracks_list.group(options)
} }
@ -151,3 +157,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,14 +1,14 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #options> <template #options>
<index-button-list :indices="genres.indices" /> <index-button-list :indices="genres.indices" />
</template> </template>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="$t('page.genres.title')" /> <p class="title is-4" v-text="$t('page.genres.title')" />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.genres.count', { count: genres.total })" v-text="$t('page.genres.count', { count: genres.total })"
/> />
</template> </template>
@ -61,3 +61,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
@ -12,7 +12,7 @@
<nav class="level"> <nav class="level">
<p class="level-item"> <p class="level-item">
<router-link <router-link
class="button is-small is-rounded" class="button is-light is-small is-rounded"
:to="{ name: 'music-recently-added' }" :to="{ name: 'music-recently-added' }"
> >
{{ $t('page.music.show-more') }} {{ $t('page.music.show-more') }}
@ -32,7 +32,7 @@
<nav class="level"> <nav class="level">
<p class="level-item"> <p class="level-item">
<router-link <router-link
class="button is-small is-rounded" class="button is-light is-small is-rounded"
:to="{ name: 'music-recently-played' }" :to="{ name: 'music-recently-played' }"
> >
{{ $t('page.music.show-more') }} {{ $t('page.music.show-more') }}
@ -95,3 +95,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
@ -62,3 +62,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
@ -51,3 +51,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
@ -13,7 +13,7 @@
<p class="level-item"> <p class="level-item">
<router-link <router-link
:to="{ name: 'music-spotify-new-releases' }" :to="{ name: 'music-spotify-new-releases' }"
class="button is-small is-rounded" class="button is-light is-small is-rounded"
> >
{{ $t('page.spotify.music.show-more') }} {{ $t('page.spotify.music.show-more') }}
</router-link> </router-link>
@ -36,7 +36,7 @@
<p class="level-item"> <p class="level-item">
<router-link <router-link
:to="{ name: 'music-spotify-featured-playlists' }" :to="{ name: 'music-spotify-featured-playlists' }"
class="button is-small is-rounded" class="button is-light is-small is-rounded"
> >
{{ $t('page.spotify.music.show-more') }} {{ $t('page.spotify.music.show-more') }}
</router-link> </router-link>
@ -102,3 +102,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
@ -60,3 +60,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="fd-page-with-tabs">
<tabs-music /> <tabs-music />
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
@ -57,3 +57,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -1,56 +1,56 @@
<template> <template>
<div class="hero is-full-height"> <div class="hero is-full-height">
<div class="hero-body is-flex is-align-items-center"> <div v-if="track.id > 0" class="hero-body is-flex is-align-items-center">
<div class="container has-text-centered"> <div class="container has-text-centered" style="max-width: 500px">
<div v-if="track.id" class="mx-auto" style="max-width: 32rem"> <cover-artwork
<cover-artwork :url="track.artwork_url"
:url="track.artwork_url" :artist="track.artist"
:artist="track.artist" :album="track.album"
:album="track.album" class="is-clickable fd-has-shadow fd-cover-big-image"
class="is-clickable fd-has-shadow fd-cover-big-image" :class="{ 'is-masked': lyricsStore.pane }"
:class="{ 'is-masked': lyricsStore.pane }" @click="open_dialog(track)"
@click="open_dialog(track)" />
/> <lyrics-pane v-if="lyricsStore.pane" />
<lyrics-pane v-if="lyricsStore.pane" /> <control-slider
<control-slider v-model:value="track_progress"
v-model:value="track_progress" class="mt-5"
class="mt-5" :disabled="is_live"
:disabled="is_live" :max="track_progress_max"
:max="track_progress_max" :cursor="cursor"
:cursor="cursor" @change="seek"
@change="seek" @mousedown="start_dragging"
@mousedown="start_dragging" @mouseup="end_dragging"
@mouseup="end_dragging" />
/> <div class="is-flex is-justify-content-space-between">
<div class="is-flex is-justify-content-space-between"> <p class="subtitle is-7" v-text="track_elapsed_time" />
<p class="subtitle is-7" v-text="track_elapsed_time" /> <p class="subtitle is-7" v-text="track_total_time" />
<p class="subtitle is-7" v-text="track_total_time" />
</div>
<p class="title is-5" v-text="track.title" />
<p class="title is-6" v-text="track.artist" />
<p
v-if="composer"
class="subtitle is-6 has-text-grey has-text-weight-bold"
v-text="composer"
/>
<p v-if="track.album" class="subtitle is-6" v-text="track.album" />
<p
v-if="settingsStore.show_filepath_now_playing"
class="subtitle is-6 has-text-grey"
v-text="track.path"
/>
</div>
<div v-else>
<p class="title is-5" v-text="$t('page.now-playing.title')" />
<p class="subtitle" v-text="$t('page.now-playing.info')" />
</div> </div>
<p class="title is-5" v-text="track.title" />
<p class="title is-6" v-text="track.artist" />
<p
v-if="composer"
class="subtitle is-6 has-text-grey has-text-weight-bold"
v-text="composer"
/>
<p v-if="track.album" class="subtitle is-6" v-text="track.album" />
<p
v-if="settingsStore.show_filepath_now_playing"
class="subtitle is-6 has-text-grey"
v-text="track.path"
/>
</div> </div>
<modal-dialog-queue-item
:show="show_details_modal"
:item="selected_item"
@close="show_details_modal = false"
/>
</div> </div>
<div v-else class="hero-body is-flex is-align-items-center">
<div class="container has-text-centered">
<p class="title is-5" v-text="$t('page.now-playing.title')" />
<p class="subtitle" v-text="$t('page.now-playing.info')" />
</div>
</div>
<modal-dialog-queue-item
:show="show_details_modal"
:item="selected_item"
@close="show_details_modal = false"
/>
</div> </div>
</template> </template>
@ -196,3 +196,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -2,14 +2,14 @@
<div> <div>
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
<div <p
class="title is-4" class="title is-4"
v-text=" v-text="
playlist.id === 0 ? $t('page.playlists.title') : playlist.name playlist.id === 0 ? $t('page.playlists.title') : playlist.name
" "
/> />
<div <p
class="is-size-7 is-uppercase" class="heading"
v-text="$t('page.playlists.count', { count: playlists.count })" v-text="$t('page.playlists.count', { count: playlists.count })"
/> />
</template> </template>
@ -89,3 +89,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -3,10 +3,6 @@
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="playlist.name" /> <div class="title is-4" v-text="playlist.name" />
<div
class="is-size-7 is-uppercase"
v-text="$t('page.playlist.track-count', { count: tracks.count })"
/>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -23,6 +19,10 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p
class="heading has-text-centered-mobile"
v-text="$t('page.playlist.track-count', { count: tracks.count })"
/>
<list-tracks :items="tracks" :uris="uris" /> <list-tracks :items="tracks" :uris="uris" />
<modal-dialog-playlist <modal-dialog-playlist
:item="playlist" :item="playlist"
@ -90,3 +90,5 @@ export default {
} }
} }
</script> </script>
<style></style>

View File

@ -3,12 +3,6 @@
<content-with-heading> <content-with-heading>
<template #heading-left> <template #heading-left>
<div class="title is-4" v-text="playlist.name" /> <div class="title is-4" v-text="playlist.name" />
<div
class="is-size-7 is-uppercase"
v-text="
$t('page.spotify.playlist.count', { count: playlist.tracks.total })
"
/>
</template> </template>
<template #heading-right> <template #heading-right>
<div class="buttons is-centered"> <div class="buttons is-centered">
@ -25,6 +19,12 @@
</div> </div>
</template> </template>
<template #content> <template #content>
<p
class="heading has-text-centered-mobile"
v-text="
$t('page.spotify.playlist.count', { count: playlist.tracks.total })
"
/>
<list-tracks-spotify :items="tracks" :context_uri="playlist.uri" /> <list-tracks-spotify :items="tracks" :context_uri="playlist.uri" />
<VueEternalLoading v-if="offset < total" :load="load_next"> <VueEternalLoading v-if="offset < total" :load="load_next">
<template #loading> <template #loading>
@ -151,3 +151,5 @@ export default {
} }
} }
</script> </script>
<style></style>

Some files were not shown because too many files have changed in this diff Show More