diff --git a/backend/app/core/config.py b/backend/app/core/config.py
index bb551aa..ff72e67 100644
--- a/backend/app/core/config.py
+++ b/backend/app/core/config.py
@@ -91,6 +91,9 @@ class UIConfig(BaseModel):
default_theme: str = "dark"
welcome_message: str = "Paste any video or audio URL. We rip it, you download it. No accounts, no tracking."
+ theme_dark: str = "cyberpunk" # Which dark theme to use
+ theme_light: str = "light" # Which light theme to use
+ theme_default_mode: str = "dark" # Start in "dark" or "light" mode
class AdminConfig(BaseModel):
diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py
index 2868b1a..ea71bfc 100644
--- a/backend/app/routers/admin.py
+++ b/backend/app/routers/admin.py
@@ -295,6 +295,9 @@ async def get_settings(
"admin_username": config.admin.username,
"purge_enabled": config.purge.enabled,
"purge_max_age_minutes": config.purge.max_age_minutes,
+ "theme_dark": config.ui.theme_dark,
+ "theme_light": config.ui.theme_light,
+ "theme_default_mode": config.ui.theme_default_mode,
}
@@ -418,6 +421,31 @@ async def update_settings(
to_persist["purge_max_age_minutes"] = val
updated.append("purge_max_age_minutes")
+ # Theme settings
+ valid_dark = {"cyberpunk", "dark", "midnight", "hacker", "neon"}
+ valid_light = {"light", "paper", "arctic", "solarized"}
+
+ if "theme_dark" in body:
+ val = body["theme_dark"]
+ if val in valid_dark:
+ config.ui.theme_dark = val
+ to_persist["theme_dark"] = val
+ updated.append("theme_dark")
+
+ if "theme_light" in body:
+ val = body["theme_light"]
+ if val in valid_light:
+ config.ui.theme_light = val
+ to_persist["theme_light"] = val
+ updated.append("theme_light")
+
+ if "theme_default_mode" in body:
+ val = body["theme_default_mode"]
+ if val in ("dark", "light"):
+ config.ui.theme_default_mode = val
+ to_persist["theme_default_mode"] = val
+ updated.append("theme_default_mode")
+
# --- Persist to DB ---
if to_persist:
await save_settings(db, to_persist)
diff --git a/backend/app/routers/system.py b/backend/app/routers/system.py
index 4228289..26ef76c 100644
--- a/backend/app/routers/system.py
+++ b/backend/app/routers/system.py
@@ -22,6 +22,9 @@ async def public_config(request: Request) -> dict:
return {
"session_mode": config.session.mode,
"default_theme": config.ui.default_theme,
+ "theme_dark": config.ui.theme_dark,
+ "theme_light": config.ui.theme_light,
+ "theme_default_mode": config.ui.theme_default_mode,
"welcome_message": config.ui.welcome_message,
"purge_enabled": config.purge.enabled,
"max_concurrent_downloads": config.downloads.max_concurrent,
diff --git a/backend/app/services/settings.py b/backend/app/services/settings.py
index e9735b8..cd3491a 100644
--- a/backend/app/services/settings.py
+++ b/backend/app/services/settings.py
@@ -35,6 +35,9 @@ ADMIN_WRITABLE_KEYS = {
"purge_enabled",
"purge_max_age_minutes",
"api_key",
+ "theme_dark",
+ "theme_light",
+ "theme_default_mode",
}
@@ -113,6 +116,12 @@ def apply_persisted_to_config(config, settings: dict) -> None:
config.purge.privacy_retention_minutes = settings["privacy_retention_minutes"]
if "api_key" in settings:
config.server.api_key = settings["api_key"]
+ if "theme_dark" in settings:
+ config.ui.theme_dark = settings["theme_dark"]
+ if "theme_light" in settings:
+ config.ui.theme_light = settings["theme_light"]
+ if "theme_default_mode" in settings:
+ config.ui.theme_default_mode = settings["theme_default_mode"]
logger.info("Applied %d persisted settings to config", len(settings))
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index d90ba3c..746b4b2 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -71,6 +71,9 @@ export interface FormatInfo {
export interface PublicConfig {
session_mode: string
default_theme: string
+ theme_dark: string
+ theme_light: string
+ theme_default_mode: string
welcome_message: string
purge_enabled: boolean
max_concurrent_downloads: number
diff --git a/frontend/src/components/AdminPanel.vue b/frontend/src/components/AdminPanel.vue
index 181dcbf..333a0fa 100644
--- a/frontend/src/components/AdminPanel.vue
+++ b/frontend/src/components/AdminPanel.vue
@@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAdminStore } from '@/stores/admin'
import { useConfigStore } from '@/stores/config'
+import { useThemeStore } from '@/stores/theme'
import { api } from '@/api/client'
import AdminLogin from './AdminLogin.vue'
import AdminSetup from './AdminSetup.vue'
@@ -35,6 +36,11 @@ const adminUsername = ref('admin')
const purgeEnabled = ref(true)
const purgeMaxAgeMinutes = ref(1440)
+// Theme settings
+const themeDark = ref('cyberpunk')
+const themeLight = ref('light')
+const themeDefaultMode = ref('dark')
+
// Change password state
const currentPassword = ref('')
const newPassword = ref('')
@@ -89,6 +95,9 @@ async function switchTab(tab: typeof activeTab.value) {
adminUsername.value = data.admin_username ?? 'admin'
purgeEnabled.value = data.purge_enabled ?? false
purgeMaxAgeMinutes.value = data.purge_max_age_minutes ?? 1440
+ themeDark.value = data.theme_dark ?? 'cyberpunk'
+ themeLight.value = data.theme_light ?? 'light'
+ themeDefaultMode.value = data.theme_default_mode ?? 'dark'
}
} catch {
// Keep current values
@@ -111,9 +120,15 @@ async function saveAllSettings() {
admin_username: adminUsername.value,
purge_enabled: purgeEnabled.value,
purge_max_age_minutes: purgeMaxAgeMinutes.value,
+ theme_dark: themeDark.value,
+ theme_light: themeLight.value,
+ theme_default_mode: themeDefaultMode.value,
})
if (ok) {
await configStore.loadConfig()
+ // Update theme store with new admin config
+ const themeStore = useThemeStore()
+ themeStore.updateAdminConfig(themeDark.value, themeLight.value, themeDefaultMode.value as 'dark' | 'light')
settingsSaved.value = true
setTimeout(() => { settingsSaved.value = false }, 3000)
}
@@ -420,6 +435,39 @@ function formatFilesize(bytes: number | null): string {
>
+
+
+
Set the default appearance for visitors. Users can toggle dark/light mode via the header icon.
+
+
+ Default Mode
+
+
+
+ Dark Theme
+
+
+
+ Light Theme
+
+
+
+
+
When "Auto" is selected, files are converted to these formats instead of the native container.
@@ -922,6 +970,25 @@ h3 {
margin-top: var(--space-sm);
}
+.theme-settings {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ margin-top: var(--space-sm);
+}
+
+.theme-setting-row {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+}
+
+.theme-setting-label {
+ min-width: 100px;
+ font-weight: 500;
+ color: var(--color-text);
+}
+
.format-default-row {
display: flex;
align-items: center;
diff --git a/frontend/src/components/DarkModeToggle.vue b/frontend/src/components/DarkModeToggle.vue
index 81122d4..aaae9c9 100644
--- a/frontend/src/components/DarkModeToggle.vue
+++ b/frontend/src/components/DarkModeToggle.vue
@@ -1,88 +1,37 @@
-
-
-
-
-
-
-
Dark
-
-
-
-
-
Light
-
-
-
-
-
+
diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts
index be5d454..887f3ef 100644
--- a/frontend/src/stores/theme.ts
+++ b/frontend/src/stores/theme.ts
@@ -1,15 +1,17 @@
/**
* Theme Pinia store — manages theme selection and application.
*
- * Built-in themes: cyberpunk (default), dark, light
- * Custom themes: loaded via /api/themes manifest at runtime
+ * Admin sets: which dark theme, which light theme, and default mode (dark/light).
+ * Visitors: see the admin default, can toggle dark/light via header icon.
+ * Visitor preference stored in cookie (session-scoped persistence).
*
- * Persistence: localStorage key 'mrip-theme'
- * Application: sets data-theme attribute on element
+ * Built-in themes: 5 dark + 4 light = 9 total.
+ * Custom themes: loaded via /api/themes manifest at runtime.
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
+import { useConfigStore } from './config'
export interface ThemeMeta {
id: string
@@ -20,8 +22,7 @@ export interface ThemeMeta {
variant: 'dark' | 'light'
}
-const STORAGE_KEY = 'mrip-theme'
-const DEFAULT_THEME = 'cyberpunk'
+const COOKIE_KEY = 'mrip-mode'
const BUILTIN_THEMES: ThemeMeta[] = [
// Dark themes
@@ -38,76 +39,90 @@ const BUILTIN_THEMES: ThemeMeta[] = [
]
export const useThemeStore = defineStore('theme', () => {
- const currentTheme = ref(DEFAULT_THEME)
+ const currentTheme = ref('cyberpunk')
+ const currentMode = ref<'dark' | 'light'>('dark')
const customThemes = ref
([])
const customThemeCSS = ref