mirror of
https://github.com/xpltdco/media-rip.git
synced 2026-06-02 11:44:30 -06:00
GSD: M002/S01 complete — Bug fixes + header/footer rework
- Fix cancel download bug: add @click.stop, debounce with cancelling ref - Rework header: remove nav tabs, replace ThemePicker with DarkModeToggle - Add isDark computed + toggleDarkMode() to theme store - Add WelcomeMessage component above URL input, reads from public config - Add welcome_message to UIConfig and public config endpoint - Add AppFooter with app version, yt-dlp version, GitHub link - Remove SSE status dot from header - Remove connectionStatus prop from AppLayout - 5 new theme toggle tests (34 frontend tests total) - 179 backend tests still passing
This commit is contained in:
parent
6780290f07
commit
ccd863f57d
14 changed files with 249 additions and 74 deletions
|
|
@ -73,6 +73,7 @@ class UIConfig(BaseModel):
|
||||||
"""UI preferences."""
|
"""UI preferences."""
|
||||||
|
|
||||||
default_theme: str = "dark"
|
default_theme: str = "dark"
|
||||||
|
welcome_message: str = "Paste any video or audio URL. We rip it, you download it. No accounts, no tracking."
|
||||||
|
|
||||||
|
|
||||||
class AdminConfig(BaseModel):
|
class AdminConfig(BaseModel):
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ async def public_config(request: Request) -> dict:
|
||||||
return {
|
return {
|
||||||
"session_mode": config.session.mode,
|
"session_mode": config.session.mode,
|
||||||
"default_theme": config.ui.default_theme,
|
"default_theme": config.ui.default_theme,
|
||||||
|
"welcome_message": config.ui.welcome_message,
|
||||||
"purge_enabled": config.purge.enabled,
|
"purge_enabled": config.purge.enabled,
|
||||||
"max_concurrent_downloads": config.downloads.max_concurrent,
|
"max_concurrent_downloads": config.downloads.max_concurrent,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ import { useSSE } from '@/composables/useSSE'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
|
import AppFooter from '@/components/AppFooter.vue'
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
const { connectionStatus, connect } = useSSE()
|
const { connect } = useSSE()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
themeStore.init()
|
themeStore.init()
|
||||||
|
|
@ -18,34 +19,7 @@ onMounted(async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppHeader :connection-status="connectionStatus" />
|
<AppHeader />
|
||||||
<nav class="app-nav">
|
|
||||||
<router-link to="/">Downloads</router-link>
|
|
||||||
<router-link to="/admin">Admin</router-link>
|
|
||||||
</nav>
|
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<AppFooter />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.app-nav {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-md);
|
|
||||||
max-width: var(--content-max-width);
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: var(--space-sm) var(--space-md);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-nav a {
|
|
||||||
padding: var(--space-xs) var(--space-sm);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-nav a.router-link-active {
|
|
||||||
color: var(--color-accent);
|
|
||||||
border-bottom: 2px solid var(--color-accent);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export interface FormatInfo {
|
||||||
export interface PublicConfig {
|
export interface PublicConfig {
|
||||||
session_mode: string
|
session_mode: string
|
||||||
default_theme: string
|
default_theme: string
|
||||||
|
welcome_message: string
|
||||||
purge_enabled: boolean
|
purge_enabled: boolean
|
||||||
max_concurrent_downloads: number
|
max_concurrent_downloads: number
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
frontend/src/components/AppFooter.vue
Normal file
57
frontend/src/components/AppFooter.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api/client'
|
||||||
|
|
||||||
|
const appVersion = ref('')
|
||||||
|
const ytDlpVersion = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const health = await api.getHealth()
|
||||||
|
appVersion.value = health.version
|
||||||
|
ytDlpVersion.value = health.yt_dlp_version
|
||||||
|
} catch {
|
||||||
|
appVersion.value = '?.?.?'
|
||||||
|
ytDlpVersion.value = 'unknown'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<footer v-if="appVersion" class="app-footer">
|
||||||
|
<span>media.rip() v{{ appVersion }}</span>
|
||||||
|
<span class="sep">|</span>
|
||||||
|
<span>yt-dlp {{ ytDlpVersion }}</span>
|
||||||
|
<span class="sep">|</span>
|
||||||
|
<a
|
||||||
|
href="https://github.com/jlightner/media-rip"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>GitHub</a>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-lg) var(--space-md);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep {
|
||||||
|
margin: 0 var(--space-sm);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer a {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer a:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ConnectionStatus } from '@/composables/useSSE'
|
import DarkModeToggle from '@/components/DarkModeToggle.vue'
|
||||||
import ThemePicker from '@/components/ThemePicker.vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
connectionStatus: ConnectionStatus
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const statusColor: Record<ConnectionStatus, string> = {
|
|
||||||
connected: 'var(--color-success)',
|
|
||||||
connecting: 'var(--color-warning)',
|
|
||||||
reconnecting: 'var(--color-warning)',
|
|
||||||
disconnected: 'var(--color-error)',
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -21,13 +9,7 @@ const statusColor: Record<ConnectionStatus, string> = {
|
||||||
<span class="title-media">media</span><span class="title-dot">.</span><span class="title-rip">rip</span><span class="title-parens">()</span>
|
<span class="title-media">media</span><span class="title-dot">.</span><span class="title-rip">rip</span><span class="title-parens">()</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<ThemePicker />
|
<DarkModeToggle />
|
||||||
<div class="header-status" :title="`SSE: ${connectionStatus}`">
|
|
||||||
<span
|
|
||||||
class="status-dot"
|
|
||||||
:style="{ backgroundColor: statusColor[connectionStatus] }"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -72,17 +54,4 @@ const statusColor: Record<ConnectionStatus, string> = {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import type { ConnectionStatus } from '@/composables/useSSE'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
connectionStatus: ConnectionStatus
|
|
||||||
}>()
|
|
||||||
|
|
||||||
type MobileTab = 'submit' | 'queue'
|
type MobileTab = 'submit' | 'queue'
|
||||||
const activeTab = ref<MobileTab>('submit')
|
const activeTab = ref<MobileTab>('submit')
|
||||||
|
|
|
||||||
54
frontend/src/components/DarkModeToggle.vue
Normal file
54
frontend/src/components/DarkModeToggle.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
|
const theme = useThemeStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="dark-mode-toggle"
|
||||||
|
:title="theme.isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||||
|
@click="theme.toggleDarkMode()"
|
||||||
|
aria-label="Toggle dark/light mode"
|
||||||
|
>
|
||||||
|
<!-- Sun icon (shown in dark mode — click to go light) -->
|
||||||
|
<svg v-if="theme.isDark" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="5"/>
|
||||||
|
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||||
|
<line x1="12" y1="21" x2="12" y2="23"/>
|
||||||
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||||||
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||||||
|
<line x1="1" y1="12" x2="3" y2="12"/>
|
||||||
|
<line x1="21" y1="12" x2="23" y2="12"/>
|
||||||
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||||
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||||
|
</svg>
|
||||||
|
<!-- Moon icon (shown in light mode — click to go dark) -->
|
||||||
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dark-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-normal);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode-toggle:hover {
|
||||||
|
color: var(--color-accent);
|
||||||
|
border-color: var(--color-accent);
|
||||||
|
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useDownloadsStore } from '@/stores/downloads'
|
import { useDownloadsStore } from '@/stores/downloads'
|
||||||
import ProgressBar from './ProgressBar.vue'
|
import ProgressBar from './ProgressBar.vue'
|
||||||
import type { Job, JobStatus } from '@/api/types'
|
import type { Job, JobStatus } from '@/api/types'
|
||||||
|
|
@ -43,11 +43,17 @@ const showProgress = computed(() =>
|
||||||
props.job.status === 'downloading' || props.job.status === 'extracting',
|
props.job.status === 'downloading' || props.job.status === 'extracting',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cancelling = ref(false)
|
||||||
|
|
||||||
async function cancel(): Promise<void> {
|
async function cancel(): Promise<void> {
|
||||||
|
if (cancelling.value) return
|
||||||
|
cancelling.value = true
|
||||||
try {
|
try {
|
||||||
await store.cancelDownload(props.job.id)
|
await store.cancelDownload(props.job.id)
|
||||||
} catch {
|
} catch (err) {
|
||||||
// Error will show in UI via store
|
console.error('[DownloadItem] Cancel failed:', err)
|
||||||
|
} finally {
|
||||||
|
cancelling.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -74,10 +80,12 @@ async function cancel(): Promise<void> {
|
||||||
<button
|
<button
|
||||||
v-if="isActive"
|
v-if="isActive"
|
||||||
class="btn-cancel"
|
class="btn-cancel"
|
||||||
@click="cancel"
|
:class="{ cancelling }"
|
||||||
|
:disabled="cancelling"
|
||||||
|
@click.stop="cancel"
|
||||||
title="Cancel download"
|
title="Cancel download"
|
||||||
>
|
>
|
||||||
✕
|
{{ cancelling ? '…' : '✕' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppLayout from '@/components/AppLayout.vue'
|
import AppLayout from '@/components/AppLayout.vue'
|
||||||
|
import WelcomeMessage from '@/components/WelcomeMessage.vue'
|
||||||
import UrlInput from '@/components/UrlInput.vue'
|
import UrlInput from '@/components/UrlInput.vue'
|
||||||
import DownloadQueue from '@/components/DownloadQueue.vue'
|
import DownloadQueue from '@/components/DownloadQueue.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout connection-status="connected">
|
<AppLayout>
|
||||||
<template #url-input>
|
<template #url-input>
|
||||||
|
<WelcomeMessage />
|
||||||
<UrlInput />
|
<UrlInput />
|
||||||
</template>
|
</template>
|
||||||
<template #queue>
|
<template #queue>
|
||||||
|
|
|
||||||
44
frontend/src/components/WelcomeMessage.vue
Normal file
44
frontend/src/components/WelcomeMessage.vue
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api/client'
|
||||||
|
|
||||||
|
const message = ref('Paste any video or audio URL. We rip it, you download it. No accounts, no tracking.')
|
||||||
|
const visible = ref(true)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const config = await api.getPublicConfig()
|
||||||
|
if (config.welcome_message !== undefined && config.welcome_message !== null) {
|
||||||
|
if (config.welcome_message === '') {
|
||||||
|
visible.value = false
|
||||||
|
} else {
|
||||||
|
message.value = config.welcome_message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Use default message on error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="visible" class="welcome-message">
|
||||||
|
<p>{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.welcome-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-md) var(--space-lg);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-message p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -33,6 +33,9 @@ export const useThemeStore = defineStore('theme', () => {
|
||||||
const customThemes = ref<ThemeMeta[]>([])
|
const customThemes = ref<ThemeMeta[]>([])
|
||||||
const customThemeCSS = ref<Map<string, string>>(new Map())
|
const customThemeCSS = ref<Map<string, string>>(new Map())
|
||||||
|
|
||||||
|
/** Whether the current theme is a dark variant (cyberpunk and dark are dark; light is light). */
|
||||||
|
const isDark = computed(() => currentTheme.value !== 'light')
|
||||||
|
|
||||||
const allThemes = computed<ThemeMeta[]>(() => [
|
const allThemes = computed<ThemeMeta[]>(() => [
|
||||||
...BUILTIN_THEMES,
|
...BUILTIN_THEMES,
|
||||||
...customThemes.value,
|
...customThemes.value,
|
||||||
|
|
@ -55,6 +58,20 @@ export const useThemeStore = defineStore('theme', () => {
|
||||||
_apply(currentTheme.value)
|
_apply(currentTheme.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle between the current theme's dark and light variant.
|
||||||
|
* Cyberpunk (default) ↔ Light. Dark ↔ Light.
|
||||||
|
*/
|
||||||
|
function toggleDarkMode(): void {
|
||||||
|
if (isDark.value) {
|
||||||
|
setTheme('light')
|
||||||
|
} else {
|
||||||
|
// Return to the last dark theme, defaulting to cyberpunk
|
||||||
|
const lastDark = localStorage.getItem(STORAGE_KEY + '-dark') || DEFAULT_THEME
|
||||||
|
setTheme(lastDark === 'light' ? DEFAULT_THEME : lastDark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch to a theme by ID. Saves to localStorage and applies immediately.
|
* Switch to a theme by ID. Saves to localStorage and applies immediately.
|
||||||
*/
|
*/
|
||||||
|
|
@ -64,6 +81,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||||
|
|
||||||
currentTheme.value = themeId
|
currentTheme.value = themeId
|
||||||
localStorage.setItem(STORAGE_KEY, themeId)
|
localStorage.setItem(STORAGE_KEY, themeId)
|
||||||
|
// Remember the last dark theme for toggle
|
||||||
|
if (themeId !== 'light') {
|
||||||
|
localStorage.setItem(STORAGE_KEY + '-dark', themeId)
|
||||||
|
}
|
||||||
_apply(themeId)
|
_apply(themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,8 +160,10 @@ export const useThemeStore = defineStore('theme', () => {
|
||||||
customThemes,
|
customThemes,
|
||||||
allThemes,
|
allThemes,
|
||||||
currentMeta,
|
currentMeta,
|
||||||
|
isDark,
|
||||||
init,
|
init,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
toggleDarkMode,
|
||||||
loadCustomThemes,
|
loadCustomThemes,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ describe('config store', () => {
|
||||||
const mockConfig = {
|
const mockConfig = {
|
||||||
session_mode: 'isolated',
|
session_mode: 'isolated',
|
||||||
default_theme: 'dark',
|
default_theme: 'dark',
|
||||||
|
welcome_message: 'Test welcome',
|
||||||
purge_enabled: false,
|
purge_enabled: false,
|
||||||
max_concurrent_downloads: 3,
|
max_concurrent_downloads: 3,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,4 +90,49 @@ describe('theme store', () => {
|
||||||
expect(store.currentMeta?.id).toBe('cyberpunk')
|
expect(store.currentMeta?.id).toBe('cyberpunk')
|
||||||
expect(store.currentMeta?.name).toBe('Cyberpunk')
|
expect(store.currentMeta?.name).toBe('Cyberpunk')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('isDark is true for cyberpunk and dark themes', () => {
|
||||||
|
const store = useThemeStore()
|
||||||
|
store.init()
|
||||||
|
expect(store.isDark).toBe(true)
|
||||||
|
|
||||||
|
store.setTheme('dark')
|
||||||
|
expect(store.isDark).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('isDark is false for light theme', () => {
|
||||||
|
const store = useThemeStore()
|
||||||
|
store.init()
|
||||||
|
store.setTheme('light')
|
||||||
|
expect(store.isDark).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggleDarkMode switches from dark to light', () => {
|
||||||
|
const store = useThemeStore()
|
||||||
|
store.init() // starts on cyberpunk (dark)
|
||||||
|
store.toggleDarkMode()
|
||||||
|
expect(store.currentTheme).toBe('light')
|
||||||
|
expect(store.isDark).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggleDarkMode switches from light back to last dark theme', () => {
|
||||||
|
const store = useThemeStore()
|
||||||
|
store.init()
|
||||||
|
// Start on cyberpunk, toggle to light, toggle back
|
||||||
|
store.toggleDarkMode()
|
||||||
|
expect(store.currentTheme).toBe('light')
|
||||||
|
store.toggleDarkMode()
|
||||||
|
expect(store.currentTheme).toBe('cyberpunk')
|
||||||
|
expect(store.isDark).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggleDarkMode remembers dark theme when starting from dark', () => {
|
||||||
|
const store = useThemeStore()
|
||||||
|
store.init()
|
||||||
|
store.setTheme('dark') // switch to the "dark" theme (not cyberpunk)
|
||||||
|
store.toggleDarkMode()
|
||||||
|
expect(store.currentTheme).toBe('light')
|
||||||
|
store.toggleDarkMode()
|
||||||
|
expect(store.currentTheme).toBe('dark') // returns to dark, not cyberpunk
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue