mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-09 10:51:37 +00:00
added server styling settings
This commit is contained in:
parent
76ea038f5a
commit
32ab726fe9
11 changed files with 466 additions and 3 deletions
|
|
@ -7,6 +7,92 @@
|
|||
:root {
|
||||
--bookshelf-texture-img: url(~static/textures/wood_default.jpg);
|
||||
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||
|
||||
/* Theme Colors */
|
||||
--color-primary: #1e293b;
|
||||
--color-primary-dark: #0f172a;
|
||||
--color-success: #22c55e;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
--color-info: #3b82f6;
|
||||
--color-bg: #111827;
|
||||
}
|
||||
|
||||
/* Color Classes */
|
||||
.bg-primary {
|
||||
background-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.bg-primary-dark {
|
||||
background-color: var(--color-primary-dark) !important;
|
||||
}
|
||||
|
||||
.bg-success {
|
||||
background-color: var(--color-success) !important;
|
||||
}
|
||||
|
||||
.bg-warning {
|
||||
background-color: var(--color-warning) !important;
|
||||
}
|
||||
|
||||
.bg-error {
|
||||
background-color: var(--color-error) !important;
|
||||
}
|
||||
|
||||
.bg-info {
|
||||
background-color: var(--color-info) !important;
|
||||
}
|
||||
|
||||
.bg-bg {
|
||||
background-color: var(--color-bg) !important;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.text-primary-dark {
|
||||
color: var(--color-primary-dark) !important;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: var(--color-success) !important;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: var(--color-warning) !important;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: var(--color-error) !important;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: var(--color-info) !important;
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border-color: var(--color-primary) !important;
|
||||
}
|
||||
|
||||
.border-primary-dark {
|
||||
border-color: var(--color-primary-dark) !important;
|
||||
}
|
||||
|
||||
.border-success {
|
||||
border-color: var(--color-success) !important;
|
||||
}
|
||||
|
||||
.border-warning {
|
||||
border-color: var(--color-warning) !important;
|
||||
}
|
||||
|
||||
.border-error {
|
||||
border-color: var(--color-error) !important;
|
||||
}
|
||||
|
||||
.border-info {
|
||||
border-color: var(--color-info) !important;
|
||||
}
|
||||
|
||||
.page {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary/30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary/70' : 'hover:bg-primary/30'">
|
||||
<p class="leading-4">{{ route.title }}</p>
|
||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
<div v-show="routeName === route.id" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
|
||||
|
|
@ -109,6 +109,11 @@ export default {
|
|||
id: 'config-authentication',
|
||||
title: this.$strings.HeaderAuthentication,
|
||||
path: '/config/authentication'
|
||||
},
|
||||
{
|
||||
id: 'styling',
|
||||
title: this.$strings.HeaderStyling,
|
||||
path: '/config/styling'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
53
client/components/app/GlobalStyling.vue
Normal file
53
client/components/app/GlobalStyling.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'GlobalStyling',
|
||||
computed: {
|
||||
...mapGetters(['getServerStyling'])
|
||||
},
|
||||
methods: {
|
||||
applyColors() {
|
||||
const styling = this.getServerStyling
|
||||
if (!styling) return
|
||||
|
||||
// Create a style element if it doesn't exist
|
||||
let styleEl = document.getElementById('custom-colors')
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style')
|
||||
styleEl.id = 'custom-colors'
|
||||
document.head.appendChild(styleEl)
|
||||
}
|
||||
|
||||
// Update CSS variables
|
||||
const css = `
|
||||
:root {
|
||||
--color-primary: ${styling.primary};
|
||||
--color-primary-dark: ${styling.primaryDark};
|
||||
--color-success: ${styling.success};
|
||||
--color-warning: ${styling.warning};
|
||||
--color-error: ${styling.error};
|
||||
--color-info: ${styling.info};
|
||||
--color-bg: ${styling.background};
|
||||
}
|
||||
`
|
||||
styleEl.innerHTML = css
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.applyColors()
|
||||
},
|
||||
watch: {
|
||||
getServerStyling: {
|
||||
handler() {
|
||||
this.applyColors()
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
68
client/components/app/ServerSettingsTab.vue
Normal file
68
client/components/app/ServerSettingsTab.vue
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="w-full">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<nuxt-link v-for="setting in serverSettings" :key="setting.path" :to="setting.path" class="p-4 bg-primary hover:bg-primary-600 rounded-lg transition-colors duration-200">
|
||||
<div class="flex items-center">
|
||||
<span class="material-symbols text-2xl mr-3">{{ setting.icon }}</span>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">{{ setting.title }}</h3>
|
||||
<p class="text-sm text-gray-300">{{ setting.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
serverSettings: [
|
||||
{
|
||||
title: this.$strings.HeaderAuthentication || 'Authentication',
|
||||
description: this.$strings.DescriptionAuthentication || 'Configure authentication methods and settings',
|
||||
path: '/config/authentication',
|
||||
icon: 'security'
|
||||
},
|
||||
{
|
||||
title: this.$strings.HeaderLibraries || 'Libraries',
|
||||
description: this.$strings.DescriptionLibraries || 'Manage your libraries',
|
||||
path: '/config/libraries',
|
||||
icon: 'library_books'
|
||||
},
|
||||
{
|
||||
title: this.$strings.HeaderBackups || 'Backups',
|
||||
description: this.$strings.DescriptionBackups || 'Configure server backups',
|
||||
path: '/config/backups',
|
||||
icon: 'backup'
|
||||
},
|
||||
{
|
||||
title: this.$strings.HeaderEmail || 'Email',
|
||||
description: this.$strings.DescriptionEmail || 'Configure email settings',
|
||||
path: '/config/email',
|
||||
icon: 'mail'
|
||||
},
|
||||
{
|
||||
title: this.$strings.HeaderNotifications || 'Notifications',
|
||||
description: this.$strings.DescriptionNotifications || 'Configure notification settings',
|
||||
path: '/config/notifications',
|
||||
icon: 'notifications'
|
||||
},
|
||||
{
|
||||
title: this.$strings.HeaderServerLogs || 'Server Logs',
|
||||
description: this.$strings.DescriptionServerLogs || 'View server logs',
|
||||
path: '/config/log',
|
||||
icon: 'article'
|
||||
},
|
||||
{
|
||||
title: this.$strings.HeaderServerStyling || 'Server Styling',
|
||||
description: this.$strings.DescriptionServerStyling || 'Customize server appearance',
|
||||
path: '/config/styling',
|
||||
icon: 'palette'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div class="text-white max-h-screen h-screen overflow-hidden bg-bg">
|
||||
<app-global-styling />
|
||||
<app-appbar />
|
||||
|
||||
<app-side-rail v-if="isShowingSideRail" class="hidden md:block" />
|
||||
|
|
@ -27,7 +28,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import AppGlobalStyling from '~/components/app/GlobalStyling.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AppGlobalStyling
|
||||
},
|
||||
middleware: 'authenticated',
|
||||
data() {
|
||||
return {
|
||||
|
|
|
|||
164
client/pages/config/styling.vue
Normal file
164
client/pages/config/styling.vue
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<div>
|
||||
<app-settings-content :header-text="$strings.HeaderServerStyling || 'Server Styling'">
|
||||
<div class="w-full max-w-3xl">
|
||||
<div class="pt-4">
|
||||
<h2 class="font-semibold">Theme Colors</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">Customize the appearance of your server by adjusting these colors.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
|
||||
<!-- Primary Color -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Primary Color</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.primary" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('primary', $event.target.value)" />
|
||||
<input type="text" v-model="colors.primary" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('primary', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Dark -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Primary Dark</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.primaryDark" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('primaryDark', $event.target.value)" />
|
||||
<input type="text" v-model="colors.primaryDark" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('primaryDark', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Color -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Success Color</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.success" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('success', $event.target.value)" />
|
||||
<input type="text" v-model="colors.success" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('success', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning Color -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Warning Color</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.warning" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('warning', $event.target.value)" />
|
||||
<input type="text" v-model="colors.warning" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('warning', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Color -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Error Color</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.error" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('error', $event.target.value)" />
|
||||
<input type="text" v-model="colors.error" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('error', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Color -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Info Color</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.info" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('info', $event.target.value)" />
|
||||
<input type="text" v-model="colors.info" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('info', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Color -->
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium">Background Color</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="color" v-model="colors.background" class="w-10 h-10 rounded cursor-pointer border border-gray-600" @input="updateColor('background', $event.target.value)" />
|
||||
<input type="text" v-model="colors.background" class="flex-1 bg-primary/20 border border-gray-600 rounded px-3 py-2" @input="updateColor('background', $event.target.value)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Save Button -->
|
||||
<div class="mt-8 flex justify-end">
|
||||
<ui-btn color="bg-success" :loading="saving" @click="saveSettings">Save Changes</ui-btn>
|
||||
</div>
|
||||
|
||||
<!-- Preview Section -->
|
||||
<div class="mt-12 border-t border-gray-600 pt-8">
|
||||
<h2 class="font-semibold mb-4">Preview</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Buttons -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium mb-2">Buttons</p>
|
||||
<ui-btn color="bg-primary" class="mr-2">Primary</ui-btn>
|
||||
<ui-btn color="bg-success" class="mr-2">Success</ui-btn>
|
||||
<ui-btn color="bg-warning" class="mr-2">Warning</ui-btn>
|
||||
<ui-btn color="bg-error">Error</ui-btn>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div class="space-y-2">
|
||||
<p class="text-sm font-medium mb-2">Alerts</p>
|
||||
<widgets-alert type="info" class="mb-2">This is an info alert</widgets-alert>
|
||||
<widgets-alert type="success" class="mb-2">This is a success alert</widgets-alert>
|
||||
<widgets-alert type="warning" class="mb-2">This is a warning alert</widgets-alert>
|
||||
<widgets-alert type="error">This is an error alert</widgets-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'styling',
|
||||
asyncData({ store, redirect }) {
|
||||
if (!store.getters['user/getIsAdminOrUp']) {
|
||||
redirect('/')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
colors: {
|
||||
primary: '#1e293b',
|
||||
primaryDark: '#0f172a',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
info: '#3b82f6',
|
||||
background: '#111827'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['getServerStyling'])
|
||||
},
|
||||
methods: {
|
||||
updateColor(key, value) {
|
||||
// Validate hex color
|
||||
if (!/^#[0-9A-F]{6}$/i.test(value)) return
|
||||
|
||||
this.colors[key] = value
|
||||
},
|
||||
async saveSettings() {
|
||||
this.saving = true
|
||||
try {
|
||||
await this.$store.dispatch('updateServerSettings', {
|
||||
styling: this.colors
|
||||
})
|
||||
this.$toast.success('Server styling updated')
|
||||
} catch (error) {
|
||||
console.error('Failed to save styling settings:', error)
|
||||
this.$toast.error('Failed to save styling settings')
|
||||
}
|
||||
this.saving = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Load saved colors from server settings
|
||||
const serverStyling = this.getServerStyling
|
||||
if (serverStyling) {
|
||||
this.colors = { ...this.colors, ...serverStyling }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -36,6 +36,10 @@ export const getters = {
|
|||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getServerStyling: (state) => {
|
||||
if (!state.serverSettings?.styling) return null
|
||||
return state.serverSettings.styling
|
||||
},
|
||||
getLibraryItemIdStreaming: (state) => {
|
||||
return state.streamLibraryItem?.id || null
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1119,5 +1119,15 @@
|
|||
"LabelRating": "Rating",
|
||||
"LabelStarRating": "{0} stars",
|
||||
"LabelNoRating": "No rating",
|
||||
"LabelAverageRating": "Average rating: {0}"
|
||||
"LabelAverageRating": "Average rating: {0}",
|
||||
"HeaderStyling": "Styling",
|
||||
"HeaderStylingColors": "Colors",
|
||||
"HeaderStylingPreview": "Preview",
|
||||
"LabelBackgroundColor": "Background Color",
|
||||
"LabelErrorColor": "Error Color",
|
||||
"LabelInfoColor": "Info Color",
|
||||
"LabelPrimaryColor": "Primary Color",
|
||||
"LabelPrimaryDarkColor": "Primary Dark Color",
|
||||
"LabelSuccessColor": "Success Color",
|
||||
"LabelWarningColor": "Warning Color"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,25 @@
|
|||
{
|
||||
"HeaderServerStyling": "Server Styling",
|
||||
"DescriptionServerStyling": "Customize server appearance",
|
||||
"HeaderSettings": "Settings",
|
||||
"HeaderLibraries": "Libraries",
|
||||
"HeaderUsers": "Users",
|
||||
"HeaderListeningSessions": "Listening Sessions",
|
||||
"HeaderBackups": "Backups",
|
||||
"HeaderLogs": "Logs",
|
||||
"HeaderNotifications": "Notifications",
|
||||
"HeaderEmail": "Email",
|
||||
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||
"HeaderRSSFeeds": "RSS Feeds",
|
||||
"HeaderAuthentication": "Authentication",
|
||||
"HeaderLibraryStats": "Library Stats",
|
||||
"HeaderYourStats": "Your Stats",
|
||||
"DescriptionAuthentication": "Configure authentication methods and settings",
|
||||
"DescriptionLibraries": "Manage your libraries",
|
||||
"DescriptionBackups": "Configure server backups",
|
||||
"DescriptionEmail": "Configure email settings",
|
||||
"DescriptionNotifications": "Configure notification settings",
|
||||
"DescriptionServerLogs": "View server logs",
|
||||
"LabelSelectAll": "Select All",
|
||||
"LabelComments": "Comments",
|
||||
"PlaceholderAddComment": "Add a comment... (Ctrl+Enter to post)",
|
||||
|
|
|
|||
25
readme.md
25
readme.md
|
|
@ -450,3 +450,28 @@ If you are using VSCode, this project includes a couple of pre-defined targets t
|
|||
# How to Support
|
||||
|
||||
[See the incomplete "How to Support" page](https://www.audiobookshelf.org/support)
|
||||
|
||||
# Development
|
||||
|
||||
### Killing Running Processes
|
||||
When developing locally, you might need to kill running instances of the client or server. Here are some helpful commands:
|
||||
|
||||
```bash
|
||||
# Kill process on port 3000 (client)
|
||||
sudo kill $(lsof -t -i:3000)
|
||||
|
||||
# Kill process on port 3333 (server)
|
||||
sudo kill $(lsof -t -i:3333)
|
||||
```
|
||||
|
||||
You can also find and kill processes manually:
|
||||
```bash
|
||||
# List all Node.js processes
|
||||
ps aux | grep node
|
||||
|
||||
# Kill processes by PID
|
||||
kill <PID>
|
||||
|
||||
# Force kill if process won't exit
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -9,6 +9,17 @@ class ServerSettings {
|
|||
this.id = 'server-settings'
|
||||
this.tokenSecret = null
|
||||
|
||||
// Styling
|
||||
this.styling = {
|
||||
primary: '#1e293b',
|
||||
primaryDark: '#0f172a',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
info: '#3b82f6',
|
||||
background: '#111827'
|
||||
}
|
||||
|
||||
// Scanner
|
||||
this.scannerParseSubtitle = false
|
||||
this.scannerFindCovers = false
|
||||
|
|
@ -88,6 +99,15 @@ class ServerSettings {
|
|||
|
||||
construct(settings) {
|
||||
this.tokenSecret = settings.tokenSecret
|
||||
|
||||
// Styling
|
||||
if (settings.styling) {
|
||||
this.styling = {
|
||||
...this.styling,
|
||||
...settings.styling
|
||||
}
|
||||
}
|
||||
|
||||
this.scannerFindCovers = !!settings.scannerFindCovers
|
||||
this.scannerCoverProvider = settings.scannerCoverProvider || 'google'
|
||||
this.scannerParseSubtitle = settings.scannerParseSubtitle
|
||||
|
|
@ -204,6 +224,7 @@ class ServerSettings {
|
|||
return {
|
||||
id: this.id,
|
||||
tokenSecret: this.tokenSecret, // Do not return to client
|
||||
styling: { ...this.styling },
|
||||
scannerFindCovers: this.scannerFindCovers,
|
||||
scannerCoverProvider: this.scannerCoverProvider,
|
||||
scannerParseSubtitle: this.scannerParseSubtitle,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue