[web] Add the ability to change the appearance of the UI

This commit is contained in:
Alain Nussbaumer 2025-06-06 21:21:31 +02:00
parent a7d4501632
commit 4bd8736346
19 changed files with 192 additions and 154 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,7 @@
"bulma": "^1.0.4",
"luxon": "^3.6.1",
"mdi-vue": "^3.0.13",
"pinia": "^3.0.2",
"pinia": "^3.0.3",
"reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.2",
"vue": "^3.5.16",
@ -30,7 +30,7 @@
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-vue": "^10.1.0",
"eslint-plugin-vue": "^10.2.0",
"prettier": "^3.5.3",
"sass": "^1.89.1",
"vite": "^6.3.5"
@ -2512,9 +2512,9 @@
}
},
"node_modules/eslint-plugin-vue": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.1.0.tgz",
"integrity": "sha512-/VTiJ1eSfNLw6lvG9ENySbGmcVvz6wZ9nA7ZqXlLBY2RkaF15iViYKxglWiIch12KiLAj0j1iXPYU6W4wTROFA==",
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.2.0.tgz",
"integrity": "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3570,9 +3570,9 @@
}
},
"node_modules/pinia": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.2.tgz",
"integrity": "sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"

View File

@ -19,7 +19,7 @@
"bulma": "^1.0.4",
"luxon": "^3.6.1",
"mdi-vue": "^3.0.13",
"pinia": "^3.0.2",
"pinia": "^3.0.3",
"reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.2",
"vue": "^3.5.16",
@ -34,7 +34,7 @@
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-vue": "^10.1.0",
"eslint-plugin-vue": "^10.2.0",
"prettier": "^3.5.3",
"sass": "^1.89.1",
"vite": "^6.3.5"

View File

@ -36,50 +36,27 @@ export default {
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);
}
background: var(--bulma-text);
}
@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);
}
background-color: var(--bulma-border);
}
@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)
var(--bulma-text) var(--sx),
var(--bulma-border) 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)
var(--bulma-border) var(--sx),
var(--bulma-background) 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 {
--ratio: v-bind(ratio);

View File

@ -34,7 +34,7 @@ export default {
display: inline-block;
&-switch {
display: inline-block;
background: var(--bulma-grey-lighter);
background: var(--bulma-border);
border-radius: 1rem;
width: 2.25rem;
height: 1.25rem;
@ -45,7 +45,7 @@ export default {
&:before {
content: '';
display: block;
background: var(--bulma-white);
background: var(--bulma-background);
border-radius: 50%;
width: 1rem;
height: 1rem;
@ -56,12 +56,12 @@ export default {
}
}
&:hover &-switch:before {
background: var(--bulma-white);
background: var(--bulma-background);
}
&-checkbox {
position: absolute;
&:checked + .toggle-switch {
background: var(--bulma-dark);
background: var(--bulma-text);
&:before {
left: 1.125rem;

View File

@ -55,6 +55,10 @@ export default {
</script>
<style scoped>
.card-content {
max-height: calc(100vh - calc(4 * var(--bulma-navbar-height)));
overflow: auto;
}
.fade-leave-active {
transition: opacity 0.2s ease;
}
@ -69,10 +73,6 @@ export default {
.fade-leave-from {
opacity: 1;
}
.card-content {
max-height: calc(100vh - calc(4 * var(--bulma-navbar-height)));
overflow: auto;
}
.is-disabled {
cursor: not-allowed;
opacity: 0.5;
@ -80,4 +80,7 @@ export default {
pointer-events: none;
}
}
.modal-background {
background-color: rgba(10, 10, 10, 0.5);
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<nav class="navbar is-fixed-bottom" :class="{ 'is-dark': !isPlayerPage }">
<nav class="navbar is-fixed-bottom" :class="{ 'is-bottom': !isPlayerPage }">
<div class="navbar-brand is-flex-grow-1">
<control-link class="navbar-item" :to="{ name: 'queue' }">
<mdicon class="icon" name="playlist-play" />

View File

@ -1,5 +1,5 @@
<template>
<nav class="navbar is-fixed-top is-light" :style="zindex">
<nav class="navbar is-fixed-top is-top" :style="zindex">
<div class="navbar-brand is-flex-grow-1">
<control-link
v-for="menu in menus.filter((menu) => menu.show && !menu.sub)"

View File

@ -331,6 +331,12 @@
"request-failed": "Anfrage gescheitert (Status: {status} {cause} {url})"
},
"settings": {
"appearance": {
"auto": "Automatisch",
"dark": "Dunkel",
"light": "Hell",
"title": "Erscheinungsbild"
},
"artwork": {
"explanation-1": "OwnTone verarbeitet PNG- und JPEG-Artwork, welches in einer eigenen Datei in der Bibliothek, in die Dateien eingebettet oder online von Radiostationen bereitgestellt werden kann.",
"explanation-2": "Zusätzlich kann auf folgende Artwork-Anbieter zugegriffen werden:",

View File

@ -331,6 +331,12 @@
"request-failed": "Request failed (status: {status} {cause} {url})"
},
"settings": {
"appearance": {
"auto": "Auto",
"dark": "Dark",
"light": "Light",
"title": "Appearance"
},
"artwork": {
"explanation-1": "OwnTone supports PNG and JPEG artwork which is either placed as separate image files in the library, embedded in the media files or made available online by radio stations.",
"explanation-2": "In addition to that, you can enable fetching artwork from the following artwork providers:",

View File

@ -331,6 +331,12 @@
"request-failed": "La requête a échoué (status: {status} {cause} {url})"
},
"settings": {
"appearance": {
"auto": "Automatique",
"dark": "Sombre",
"light": "Clair",
"title": "Mode dapparence"
},
"artwork": {
"explanation-1": "OwnTone prend en charge les illustrations au format PNG et JPEG qui sont soit placées dans la bibliothèque en tant que fichiers image séparés, soit intégrées dans les fichiers média, soit mises à disposition en ligne par les stations de radio.",
"explanation-2": "En outre, vous pouvez activer la récupération des illustrations à partir des fournisseurs dillustrations suivants :",

View File

@ -331,6 +331,12 @@
"request-failed": "请求失败 (状态:{status} {cause} {url})"
},
"settings": {
"appearance": {
"auto": "自动",
"dark": "深色",
"light": "浅色",
"title": "外观"
},
"artwork": {
"explanation-1": "OwnTone支持PNG和 JPEG封面这些封面可以作为单独的图像文件放置在库中或嵌入到媒体文件中也可以通过广播电台在线提供",
"explanation-2": "除此之外,您还可以从以下素材提供者获取封面:",

View File

@ -331,6 +331,12 @@
"request-failed": "請求失敗 (狀態:{status} {cause} {url})"
},
"settings": {
"appearance": {
"auto": "自動",
"dark": "深色",
"light": "淺色",
"title": "外觀"
},
"artwork": {
"explanation-1": "OwnTone支持PNG和 JPEG封面這些封面可以作為單獨的圖像文件放置在庫中或嵌入到媒體文件中也可以通過電台在線提供",
"explanation-2": "除此之外,您還可以從以下素材提供者獲取封面:",

View File

@ -1,4 +1,4 @@
import './mystyles.scss'
import './styles.scss'
import App from './App.vue'
import VueClickAway from 'vue3-click-away'
import VueLazyLoad from 'vue3-lazyload'

View File

@ -1,66 +0,0 @@
@charset "utf-8";
@use 'bulma/bulma';
@use 'bulma/sass/utilities/mixins';
@media (prefers-color-scheme: dark) {
.navbar {
background-color: var(--bulma-dark);
&-item {
--bulma-navbar-item-color: hsl(
var(--bulma-navbar-invert-h),
var(--bulma-navbar-invert-s),
var(--bulma-navbar-item-color-invert-l)
);
--bulma-navbar-item-background-l: var(--bulma-light-invert-l);
}
}
}
button.navbar-item {
background-color: hsla(
var(--bulma-navbar-h),
var(--bulma-navbar-s),
calc(
var(--bulma-navbar-item-background-l) +
var(--bulma-navbar-item-background-l-delta)
),
var(--bulma-navbar-item-background-a)
);
&:hover {
--bulma-navbar-item-background-l-delta: var(
--bulma-navbar-item-hover-background-l-delta
);
--bulma-navbar-item-background-a: 1;
}
}
.navbar-item {
width: var(--bulma-navbar-height);
justify-content: center;
}
.media:first-of-type {
padding-top: 1rem;
}
.modal-background {
background-color: rgba(10, 10, 10, 0.5);
}
.overlay-fullscreen {
@extend .is-overlay;
z-index: 25;
background-color: rgba(10, 10, 10, 0.5);
position: fixed;
}
.dropdown-menu.is-mobile {
@include mixins.mobile {
width: 100vw;
}
.dropdown-content {
max-height: calc(100vh - calc(2 * var(--bulma-navbar-height)));
overflow: auto;
}
}

View File

@ -11,6 +11,17 @@
/>
</template>
</content-with-heading>
<content-with-heading>
<template #heading>
<pane-title :content="{ title: $t('settings.appearance.title') }" />
</template>
<template #content>
<control-dropdown
v-model:value="appearance"
:options="settingsStore.appearances"
/>
</template>
</content-with-heading>
<content-with-heading>
<template #heading>
<pane-title
@ -127,6 +138,14 @@ export default {
return { settingsStore: useSettingsStore() }
},
computed: {
appearance: {
get() {
return this.settingsStore.currentAppearance()
},
set(appearance) {
this.settingsStore.setAppearance(appearance)
}
},
locale: {
get() {
return this.settingsStore.currentLocale()

View File

@ -6,6 +6,9 @@ const { t, availableLocales } = i18n.global
export const useSettingsStore = defineStore('SettingsStore', {
actions: {
currentAppearance() {
return this.$state.appearance
},
currentLocale() {
const languages = availableLocales
let locale = languages.find((lang) => lang === i18n.global.locale.value)
@ -31,6 +34,16 @@ export const useSettingsStore = defineStore('SettingsStore', {
async initialise() {
this.$state = await settings.state()
},
setAppearance(appearance) {
this.$state.appearance = appearance
if (appearance === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark')
} else if (appearance === 'light') {
document.documentElement.setAttribute('data-theme', 'light')
} else {
document.documentElement.removeAttribute('data-theme')
}
},
setLocale(locale) {
i18n.global.locale.value = locale
},
@ -50,6 +63,13 @@ export const useSettingsStore = defineStore('SettingsStore', {
}
},
getters: {
appearances() {
return [
{ id: 'auto', name: t('settings.appearance.auto') },
{ id: 'light', name: t('settings.appearance.light') },
{ id: 'dark', name: t('settings.appearance.dark') }
]
},
locales() {
return availableLocales.map((item) => ({
id: item,
@ -81,5 +101,5 @@ export const useSettingsStore = defineStore('SettingsStore', {
showMenuItemSearch: (state) =>
state.get('webinterface', 'show_menu_item_search')?.value ?? false
},
state: () => ({ categories: [] })
state: () => ({ appearance: 'auto', categories: [] })
})

55
web-src/src/styles.scss Normal file
View File

@ -0,0 +1,55 @@
@charset "utf-8";
@use 'bulma/bulma';
@use 'bulma/sass/utilities/mixins';
@mixin navbar($theme) {
$bg: '--bulma-#{$theme}';
$bg-l: '--bulma-#{$theme}-l';
$color-l: '--bulma-#{$theme}-invert-l';
--bulma-navbar-background-color: var(#{$bg});
--bulma-navbar-item-background-l: var(#{$bg-l});
--bulma-navbar-item-color-l: var(#{$color-l});
}
@media (prefers-color-scheme: dark) {
.is-top {
@include navbar(dark);
}
}
@media (prefers-color-scheme: light) {
.is-top {
@include navbar(light);
}
}
.is-bottom {
@include navbar(dark);
}
.navbar-item {
width: var(--bulma-navbar-height);
justify-content: center;
}
.media:first-of-type {
padding-top: 1rem;
}
.overlay-fullscreen {
@extend .is-overlay;
z-index: 25;
background-color: rgba(10, 10, 10, 0.5);
position: fixed;
}
.dropdown-menu.is-mobile {
@include mixins.mobile {
width: 100vw;
}
.dropdown-content {
max-height: calc(100vh - calc(2 * var(--bulma-navbar-height)));
overflow: auto;
}
}