Refactor: re-design the settings page.

This commit is contained in:
arabcoders 2026-01-03 17:26:58 +03:00
parent 8e91eb4ac7
commit 1c5b4f344d
7 changed files with 253 additions and 200 deletions

View file

@ -33,7 +33,7 @@ FRONTEND_ROUTES: list[str] = [
"/logs/",
"/conditions/",
"/browser/",
"/settings/",
"/simple/",
"/browser/{path:.*}",
]

View file

@ -374,3 +374,7 @@ div.is-centered {
padding: 0;
margin: 0 auto;
}
.is-rounded-less {
border-radius: 0px !important;
}

View file

@ -1,51 +1,39 @@
<template>
<main class="m-6">
<div>
<template v-if="!simpleMode">
<span class="title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-cog" /></span>
<p class="card-header-title">WebUI Settings</p>
</span>
</span>
<span class="field is-horizontal" />
</template>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Page View</label>
</div>
<div class="field-body">
<div class="modal" :class="{ 'is-active': isOpen }">
<div class="modal-background" @click="emitter('close')" />
<div class="modal-card"
:class="{ 'slide-from-right': direction === 'right', 'slide-from-left': direction === 'left' }">
<header class="modal-card-head is-rounded-less">
<p class="modal-card-title">WebUI Settings</p>
<button class="delete" @click="emitter('close')" aria-label="close" />
</header>
<section class="modal-card-body">
<div class="box">
<div class="field">
<div class="control is-expanded has-icons-left">
<label class="label">Page View</label>
<div class="control">
<input id="view_mode" type="checkbox" class="switch is-success" v-model="simpleMode">
<label for="view_mode" class="is-unselectable">
{{ simpleMode ? 'Simple View' : 'Regular View' }}
</label>
</div>
<p class="help">
<span class="icon"> <i class="fa-solid fa-info-circle" /></span>
<span class="icon"><i class="fa-solid fa-info-circle" /></span>
The simple view is ideal for non-technical users and mobile devices.
</p>
</div>
</div>
</div>
<span class="field title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-palette" /></span>
<p class="card-header-title">Theming</p>
</span>
</span>
<span class="field is-horizontal" />
<div class="box">
<p class="title is-5 mb-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-palette" /></span>
<span>Theming</span>
</span>
</p>
<div class="field is-horizontal">
<div class="field-label">
<label class="label">Color scheme</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="field">
<label class="label">Color scheme</label>
<div class="control">
<label for="auto" class="radio">
<input id="auto" type="radio" v-model="selectedTheme" value="auto">
@ -64,15 +52,9 @@
</label>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Show Background</label>
</div>
<div class="field-body">
<div class="field">
<label class="label">Show Background</label>
<div class="control">
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
<label for="random_bg" class="is-unselectable">
@ -80,62 +62,42 @@
</label>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label"></label>
</div>
<div class="field-body">
<div class="field">
<div class="field" v-if="bg_enable">
<div class="control">
<template v-if="bg_enable">
<button @click="$emit('reload_bg')" class="has-text-link" :disabled="isLoading">
<span class="icon-text">
<span class="icon"><i class="fa"
:class="{ 'fa-spin fa-spinner': isLoading, 'fa-file-image': !isLoading }" /></span>
<span>Reload Background</span>
</span>
</button>
</template>
<button @click="$emit('reload_bg')" class="button is-link is-light is-fullwidth" :disabled="isLoading">
<span class="icon"><i class="fa"
:class="{ 'fa-spin fa-spinner': isLoading, 'fa-file-image': !isLoading }" /></span>
<span>Reload Background</span>
</button>
</div>
</div>
<div class="field" v-if="bg_enable">
<label class="label">Background visibility</label>
<div class="field has-addons">
<div class="control">
<a class="button is-static">
<code>{{ parseFloat(String(1.0 - bg_opacity)).toFixed(2) }}</code>
</a>
</div>
<div class="control is-expanded">
<input class="input" type="range" v-model="bg_opacity" min="0.50" max="1.00" step="0.05">
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Background visibility</label>
</div>
<div class="field-body">
<div class="field is-expanded">
<a class="button is-static is-small">
<code>{{ parseFloat(String(1.0 - bg_opacity)).toFixed(2) }}</code>
</a>
<div class="box">
<p class="title is-5 mb-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-home" /></span>
<span>Dashboard</span>
</span>
</p>
<div class="control">
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50" max="1.00"
step="0.05">
</div>
</div>
</div>
</div>
<span class="field title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-home" /></span>
<p class="card-header-title">Dashboard</p>
</span>
</span>
<span class="field is-horizontal" />
<div class="field is-horizontal" v-if="!simpleMode">
<div class="field-label is-normal">
<label class="label">URL Separator</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="field" v-if="!simpleMode">
<label class="label">URL Separator</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model="separator">
@ -146,17 +108,10 @@
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Show Thumbnails</label>
</div>
<div class="field-body">
<div class="control">
<div class="field">
<div class="field">
<label class="label">Show Thumbnails</label>
<div class="control">
<input id="show_thumbnail" type="checkbox" class="switch is-success" v-model="show_thumbnail">
<label for="show_thumbnail" class="is-unselectable">
{{ show_thumbnail ? 'Yes' : 'No' }}
@ -167,17 +122,10 @@
Show videos thumbnail if available
</p>
</div>
</div>
</div>
<div class="field is-horizontal" v-if="show_thumbnail">
<div class="field-label is-normal">
<label class="label">Aspect Ratio</label>
</div>
<div class="field-body">
<div class="control">
<div class="field">
<div class="field" v-if="show_thumbnail">
<label class="label">Aspect Ratio</label>
<div class="control">
<label for="ratio_16by9" class="radio">
<input id="ratio_16by9" type="radio" v-model="thumbnail_ratio" value="is-16by9">
<span>&nbsp;16:9</span>
@ -193,39 +141,27 @@
</p>
</div>
</div>
</div>
<span class="field title is-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-bell" /></span>
<p class="card-header-title">Notifications</p>
</span>
</span>
<span class="field is-horizontal" />
<div class="box">
<p class="title is-5 mb-4">
<span class="icon-text">
<span class="icon"><i class="fas fa-bell" /></span>
<span>Notifications</span>
</span>
</p>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">Show notifications</label>
</div>
<div class="field-body">
<div class="control">
<div class="field">
<div class="field">
<label class="label">Show notifications</label>
<div class="control">
<input id="allow_toasts" type="checkbox" class="switch is-success" v-model="allow_toasts">
<label for="allow_toasts" class="is-unselectable">
{{ allow_toasts ? 'Yes' : 'No' }}
</label>
</div>
</div>
</div>
</div>
<div class="field is-horizontal" v-if="allow_toasts">
<div class="field-label is-normal">
<label class="label">Notification target</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="field" v-if="allow_toasts">
<label class="label">Notification target</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model="toast_target" @change="onNotificationTargetChange">
@ -244,15 +180,9 @@
</template>
</p>
</div>
</div>
</div>
<div class="field is-horizontal" v-if="allow_toasts && toast_target === 'toast'">
<div class="field-label is-normal">
<label class="label">Notifications position</label>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="field" v-if="allow_toasts && toast_target === 'toast'">
<label class="label">Notifications position</label>
<div class="control">
<div class="select is-fullwidth">
<select v-model="toast_position">
@ -266,17 +196,10 @@
</div>
</div>
</div>
</div>
</div>
<div class="field is-horizontal" v-if="allow_toasts && toast_target === 'toast'">
<div class="field-label is-normal">
<label class="label">Dismiss notification on click</label>
</div>
<div class="field-body">
<div class="control">
<div class="field">
<div class="field" v-if="allow_toasts && toast_target === 'toast'">
<label class="label">Dismiss notification on click</label>
<div class="control">
<input id="dismiss_on_click" type="checkbox" class="switch is-success" v-model="toast_dismiss_on_click">
<label for="dismiss_on_click" class="is-unselectable">
{{ toast_dismiss_on_click ? 'Yes' : 'No' }}
@ -284,22 +207,31 @@
</div>
</div>
</div>
</div>
</section>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import 'assets/css/bulma-switch.css'
import { watch, onMounted, onBeforeUnmount, ref } from 'vue'
import { useStorage } from '@vueuse/core'
import { ref, onMounted } from 'vue'
import { POSITION } from 'vue-toastification'
import { useConfigStore } from '~/stores/ConfigStore'
import { useNotification } from '~/composables/useNotification'
import type { notificationTarget } from '~/composables/useNotification'
defineProps<{ isLoading: boolean }>()
const emitter = defineEmits<{ (e: 'reload_bg' | 'close'): void }>()
const props = withDefaults(defineProps<{
isOpen?: boolean
direction?: 'left' | 'right'
isLoading?: boolean
}>(), {
isOpen: false,
direction: 'right',
isLoading: false
})
const emitter = defineEmits<{ (e: 'close' | 'reload_bg'): void }>()
const config = useConfigStore()
const notification = useNotification()
@ -317,14 +249,27 @@ const separator = useStorage<string>('url_separator', separators[0]?.value ?? ',
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
const isSecureContext = ref<boolean>(false)
const handleKeydown = (e: KeyboardEvent) => {
if ('Escape' === e.key && props.isOpen) {
e.preventDefault()
e.stopPropagation()
emitter('close')
}
}
onMounted(async () => {
isSecureContext.value = window.isSecureContext
await nextTick()
if ('browser' === toast_target.value && !isSecureContext.value) {
toast_target.value = 'toast'
}
document.addEventListener('keydown', handleKeydown)
})
onBeforeUnmount(() => document.removeEventListener('keydown', handleKeydown))
const onNotificationTargetChange = async (): Promise<void> => {
if ('browser' === toast_target.value) {
const permission = await notification.requestBrowserPermission()
@ -335,5 +280,74 @@ const onNotificationTargetChange = async (): Promise<void> => {
}
}
watch(simpleMode, async () => emitter('close'))
watch(() => props.isOpen, isOpen => {
if (isOpen) {
document.body.classList.add('settings-panel-open')
} else {
document.body.classList.remove('settings-panel-open')
}
})
</script>
<style scoped>
.modal-card.slide-from-right {
position: fixed;
right: 0;
top: 0;
height: 100vh;
max-height: 100vh;
margin: 0;
width: 600px;
max-width: 90vw;
transition: transform 0.3s ease;
transform: translateX(100%);
}
.modal.is-active .modal-card.slide-from-right {
transform: translateX(0);
}
.modal-card.slide-from-left {
position: fixed;
left: 0;
top: 0;
height: 100vh;
max-height: 100vh;
margin: 0;
width: 600px;
max-width: 90vw;
transition: transform 0.3s ease;
transform: translateX(-100%);
}
.modal.is-active .modal-card.slide-from-left {
transform: translateX(0);
}
.modal-card-body {
overflow-y: auto;
}
@media screen and (max-width: 768px) {
.modal-card.slide-from-right,
.modal-card.slide-from-left {
width: 100vw;
max-width: 100vw;
}
}
:global(body.settings-panel-open) {
overflow: hidden;
}
#main_container {
transition: transform 0.3s ease;
}
@media screen and (min-width: 769px) {
:global(.settings-open #main_container) {
transform: translateX(-300px);
}
}
</style>

View file

@ -1,10 +1,11 @@
<template>
<div class="basic-wrapper">
<div class="basic-wrapper" :class="{ 'settings-open': settingsOpen }">
<div class="form-container" :class="{ 'is-centered': shouldCenterForm }">
<section class="download-form box">
<form class="download-form__body" autocomplete="off" @submit.prevent="addDownload">
<label class="label" for="download-url">
{{ greetingMessage }}
<template v-if="!isMobile">{{ greetingMessage }}</template>
<template v-else>What would you like to download?</template>
<span class="is-pulled-right">
<span class="icon is-pointer" :class="connectionStatusColor" @click="$emit('show_settings')"
v-tooltip="'WebUI Settings'">
@ -106,7 +107,9 @@
</figure>
<div class="media-content is-grid">
<p class="title is-6 mb-0 queue-title">
<NuxtLink target="_blank" :href="entry.item.url">{{ entry.item.title }}</NuxtLink>
<NuxtLink target="_blank" :href="entry.item.url" v-tooltip="entry.item.title">
{{ entry.item.title || 'Untitled' }}
</NuxtLink>
</p>
<div class="field is-grouped is-unselectable">
<div class="control">
@ -205,7 +208,6 @@
</div>
<button class="modal-close is-large" aria-label="close" @click="closePlayer"></button>
</div>
</div>
</template>
@ -225,10 +227,19 @@ import { ag, encodePath, formatTime, makeDownload, request, stripPath, ucFirst,
defineEmits<{ (e: 'show_settings'): void }>()
withDefaults(defineProps<{
settingsOpen?: boolean
}>(), {
settingsOpen: false
})
const configStore = useConfigStore()
const stateStore = useStateStore()
const socketStore = useSocketStore()
const toast = useNotification()
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const isMobile = useMediaQuery({ maxWidth: 1024 })
const { app, paused, presets } = storeToRefs(configStore)
const { queue, history } = storeToRefs(stateStore)
@ -241,12 +252,9 @@ const formUrl = ref<string>('')
const formPreset = ref<{ preset: string }>({ preset: app.value.default_preset || '' })
const addInProgress = ref<boolean>(false)
const showExtras = ref<boolean>(false)
const dlFields = useStorage<Record<string, any>>('dl_fields', {})
const show_thumbnail = useStorage<boolean>('show_thumbnail', true)
const paginationInfo = computed(() => stateStore.getPagination())
const queueItems = computed<StoreItem[]>(() => Object.values(queue.value ?? {}).slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)))
const historyEntries = computed<StoreItem[]>(() => {
const items = Object.values(history.value ?? {})
return items.slice().sort((a, b) => new Date(b.datetime).getTime() - new Date(a.datetime).getTime())
@ -654,6 +662,10 @@ onMounted(async () => {
if (route.query?.simple !== undefined) {
const simpleMode = useStorage<boolean>('simple_mode', configStore.app.simple_mode || false)
simpleMode.value = ['true', '1', 'yes', 'on'].includes(route.query.simple as string)
await nextTick()
const url = new URL(window.location.href)
url.searchParams.delete('simple')
window.history.replaceState({}, '', url.toString())
}
if (socketStore.isConnected && !paginationInfo.value.isLoaded) {
@ -718,6 +730,17 @@ useIntersectionObserver(loadMoreTrigger, ([entry]) => {
align-items: center;
gap: 2.5rem;
padding: 2rem 1rem 4rem;
transition: transform 0.3s ease;
}
.basic-wrapper.settings-open {
transform: translateX(-300px);
}
@media screen and (max-width: 768px) {
.basic-wrapper.settings-open {
transform: translateX(0);
}
}
.form-container {

View file

@ -2,38 +2,16 @@
<template v-if="simpleMode">
<Connection :status="socket.connectionStatus" @reconnect="() => socket.reconnect()" />
<Simple @show_settings="() => show_settings = true" />
<Simple @show_settings="() =>show_settings = true"
:class="{ 'settings-open': show_settings }" />
</template>
<template v-if="show_settings">
<Modal @close="closeSettings()"
:content-class="isMobile ? 'modal-content-max is-overflow-scroll ' : 'modal-content-max'">
<div class="columns is-multiline" style="width:100%">
<div class="column is-12">
<div class="card">
<header class="card-header">
<p class="card-header-title">WebUI Settings</p>
<span class="card-header-icon">
<span class="icon"><i class="fas fa-cog" /></span>
</span>
</header>
<div class="card-content">
<div class="columns is-multiline">
<div class="column is-12">
<settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)"
@close="closeSettings()" />
</div>
</div>
</div>
</div>
</div>
</div>
</Modal>
</template>
<SettingsPanel :isOpen="show_settings" :isLoading="loadingImage" @close="closeSettings()"
@reload_bg="() => loadImage(true)" direction="right" />
<template v-if="!simpleMode">
<Shutdown v-if="app_shutdown" />
<div id="main_container" class="container" v-else>
<div id="main_container" class="container" :class="{ 'settings-open': show_settings }" v-else>
<NewVersion v-if="newVersionIsAvailable" />
<Connection :status="socket.connectionStatus" @reconnect="() => socket.reconnect()" />
<nav class="navbar is-mobile is-dark">
@ -142,11 +120,11 @@
</div>
<div class="navbar-item">
<NuxtLink class="button is-dark " :class="{ 'has-tooltip-bottom mr-4': !isMobile }" to="/settings"
@click.prevent="(e: MouseEvent) => changeRoute(e)">
<button class="button is-dark " :class="{ 'has-tooltip-bottom mr-4': !isMobile }"
@click="show_settings = !show_settings">
<span class="icon"><i class="fas fa-cog" /></span>
<span v-if="isMobile">WebUI Settings</span>
</NuxtLink>
</button>
</div>
</div>
@ -226,7 +204,6 @@ import Simple from '~/components/Simple.vue'
import Shutdown from '~/components/shutdown.vue'
import Markdown from '~/components/Markdown.vue'
import Connection from '~/components/Connection.vue'
import Settings from "~/pages/settings.vue";
const Year = new Date().getFullYear()
const selectedTheme = useStorage('theme', 'auto')
@ -241,7 +218,6 @@ const isMobile = useMediaQuery({ maxWidth: 1024 })
const app_shutdown = ref<boolean>(false)
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
const show_settings = ref(false)
const doc = ref<{ file: string }>({ file: '' })
const applyPreferredColorScheme = (scheme: string) => {
@ -525,3 +501,23 @@ const connectionStatusColor = computed(() => {
const scrollToTop = () => document.getElementById('top')?.scrollIntoView({ behavior: 'smooth' });
</script>
<style>
#main_container,
.basic-wrapper {
transition: transform 0.3s ease, margin-right 0.3s ease;
}
#main_container.settings-open,
.basic-wrapper.settings-open {
transform: translateX(-300px);
}
@media screen and (max-width: 768px) {
#main_container.settings-open,
.basic-wrapper.settings-open {
transform: translateX(0);
}
}
</style>

View file

@ -163,12 +163,16 @@ watch(() => route.query.tab, (newTab) => {
activeTab.value = newTab as TabType
})
onMounted(() => {
onMounted(async () => {
const route = useRoute()
if (route.query?.simple !== undefined) {
const simpleMode = useStorage<boolean>('simple_mode', config.app.simple_mode || false)
simpleMode.value = ['true', '1', 'yes', 'on'].includes(route.query.simple as string)
await nextTick()
const url = new URL(window.location.href)
url.searchParams.delete('simple')
window.history.replaceState({}, '', url.toString())
}
activeTab.value = getInitialTab()

12
ui/app/pages/simple.vue Normal file
View file

@ -0,0 +1,12 @@
<template></template>
<script lang="ts" setup>
import { useStorage } from '@vueuse/core'
const simpleMode = useStorage<boolean>('simple_mode', true)
onMounted(async () => {
simpleMode.value = true
await nextTick()
await navigateTo('/')
})
</script>