mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat: settings page — slide-over drawer (option B)
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Library, BookMarked, Clock, BookOpen } from 'lucide-vue-next'
|
||||
import { Library, BookMarked, Clock, BookOpen, Settings } from 'lucide-vue-next'
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
@@ -12,6 +13,9 @@ import {
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { useSettingsDrawer } from '@/composables/useSettingsDrawer'
|
||||
|
||||
const { open: openSettings } = useSettingsDrawer()
|
||||
|
||||
const navItems = [
|
||||
{ id: 'library', label: 'All Books', icon: Library, active: true },
|
||||
@@ -76,6 +80,17 @@ const navItems = [
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter class="border-t border-sidebar-border">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton tooltip="Settings" class="gap-2.5" @click="openSettings">
|
||||
<Settings :size="15" class="text-sidebar-foreground/50" />
|
||||
<span class="text-sidebar-foreground/70">Settings</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarFooter>
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
export function useSettingsDrawer() {
|
||||
return {
|
||||
isOpen,
|
||||
open: () => (isOpen.value = true),
|
||||
close: () => (isOpen.value = false),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { BookOpen, Database, Github, Server } from 'lucide-vue-next'
|
||||
|
||||
const stack = [
|
||||
{ label: 'Frontend', value: 'Vue 3 + TypeScript + Tailwind CSS v4' },
|
||||
{ label: 'Backend', value: 'NestJS 11 + Fastify' },
|
||||
{ label: 'Database', value: 'PostgreSQL + Drizzle ORM' },
|
||||
{ label: 'EPUB Engine', value: 'Foliate (epub.js)' },
|
||||
{ label: 'PDF Engine', value: 'PDF.js' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">About</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Information about this application.</p>
|
||||
</div>
|
||||
|
||||
<!-- App identity -->
|
||||
<div class="mb-6">
|
||||
<div class="border border-border rounded-lg bg-card px-5 py-5 flex items-start gap-4">
|
||||
<div class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
<BookOpen :size="22" class="text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-base font-semibold text-foreground font-serif">projectx</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Version 0.1.0 — early development</p>
|
||||
<p class="text-xs text-muted-foreground mt-2 leading-relaxed max-w-lg">
|
||||
A self-hosted book and library management application with support for EPUB, PDF, and comic formats.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tech stack -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Tech Stack</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div v-for="item in stack" :key="item.label" class="flex items-center justify-between px-5 py-3.5 bg-card">
|
||||
<p class="text-sm text-muted-foreground">{{ item.label }}</p>
|
||||
<p class="text-sm font-medium text-foreground">{{ item.value }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server info -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Server</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center gap-3 px-5 py-4 bg-card">
|
||||
<Server :size="15" class="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">API</p>
|
||||
<p class="text-xs text-muted-foreground font-mono mt-0.5">/api</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-5 py-4 bg-card">
|
||||
<Database :size="15" class="text-muted-foreground shrink-0" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Database</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">PostgreSQL</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Links</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<a
|
||||
href="https://github.com/neonsolstice/projectx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-3 px-5 py-4 bg-card hover:bg-muted/50 transition-colors group"
|
||||
>
|
||||
<Github :size="15" class="text-muted-foreground shrink-0" />
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-foreground group-hover:text-primary transition-colors">GitHub</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Source code and issue tracker</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { ACCENT_OPTIONS, RADIUS_OPTIONS, useThemeStore } from '@/stores/theme'
|
||||
import { Moon, Sun } from 'lucide-vue-next'
|
||||
import { useDisplaySettings } from '@/composables/useDisplaySettings'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { coverSize, gridGap } = useDisplaySettings()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Appearance</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Customize how the app looks and feels.</p>
|
||||
</div>
|
||||
|
||||
<!-- Theme & colors -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Theme</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<!-- Light / dark -->
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Color scheme</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Light or dark interface</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 p-1 rounded-lg border border-border bg-muted/50">
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="themeStore.theme === 'light' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="themeStore.theme === 'dark' && themeStore.toggleTheme()"
|
||||
>
|
||||
<Sun :size="12" /> Light
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="themeStore.theme === 'dark' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="themeStore.theme === 'light' && themeStore.toggleTheme()"
|
||||
>
|
||||
<Moon :size="12" /> Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Accent color -->
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Accent color</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Controls highlights and interactive elements</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-for="opt in ACCENT_OPTIONS"
|
||||
:key="opt.id"
|
||||
:title="opt.label"
|
||||
class="w-6 h-6 rounded-full transition-all hover:scale-110 focus:outline-none"
|
||||
:style="{
|
||||
backgroundColor: opt.color,
|
||||
outline: themeStore.accent === opt.id ? `2px solid ${opt.color}` : 'none',
|
||||
outlineOffset: '2px',
|
||||
transform: themeStore.accent === opt.id ? 'scale(1.25)' : '',
|
||||
}"
|
||||
@click="themeStore.setAccent(opt.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Corner radius -->
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Corner radius</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Roundness of cards and UI elements</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
v-for="opt in RADIUS_OPTIONS"
|
||||
:key="opt.id"
|
||||
:title="opt.label"
|
||||
class="h-7 px-3 text-xs border-2 transition-colors font-medium"
|
||||
:style="{ borderRadius: opt.id === 'sharp' ? '2px' : opt.id === 'default' ? '6px' : '14px' }"
|
||||
:class="
|
||||
themeStore.radius === opt.id
|
||||
? 'border-primary text-primary bg-primary/8'
|
||||
: 'border-border text-muted-foreground hover:border-muted-foreground/40 hover:text-foreground'
|
||||
"
|
||||
@click="themeStore.setRadius(opt.id)"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Library view -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Library View</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<!-- Cover size -->
|
||||
<div class="px-5 py-4 bg-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Cover size</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Width of book covers in the grid</p>
|
||||
</div>
|
||||
<span class="text-sm font-medium tabular-nums text-foreground">{{ coverSize }}px</span>
|
||||
</div>
|
||||
<input
|
||||
:value="coverSize"
|
||||
@input="coverSize = Number(($event.target as HTMLInputElement).value)"
|
||||
type="range"
|
||||
min="80"
|
||||
max="280"
|
||||
step="10"
|
||||
class="w-full accent-primary cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Grid gap -->
|
||||
<div class="px-5 py-4 bg-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Grid spacing</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Gap between covers in the grid</p>
|
||||
</div>
|
||||
<span class="text-sm font-medium tabular-nums text-foreground">{{ gridGap }}px</span>
|
||||
</div>
|
||||
<input
|
||||
:value="gridGap"
|
||||
@input="gridGap = Number(($event.target as HTMLInputElement).value)"
|
||||
type="range"
|
||||
min="4"
|
||||
max="40"
|
||||
step="4"
|
||||
class="w-full accent-primary cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const scrollMode = ref<'webtoon' | 'paged'>('paged')
|
||||
const readingDirection = ref<'ltr' | 'rtl'>('ltr')
|
||||
const background = ref<'black' | 'white' | 'system'>('black')
|
||||
const fitMode = ref<'width' | 'height' | 'original'>('width')
|
||||
const rememberPosition = ref(true)
|
||||
const preloadPages = ref(3)
|
||||
|
||||
const backgrounds = [
|
||||
{ id: 'black', label: 'Black' },
|
||||
{ id: 'white', label: 'White' },
|
||||
{ id: 'system', label: 'System' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Comics Reader</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default settings applied when opening CBZ, CBR, and CB7 files.</p>
|
||||
</div>
|
||||
|
||||
<!-- View -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">View</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Reading mode</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Paged flips one page at a time; Webtoon scrolls vertically</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 p-1 rounded-lg border border-border bg-muted/50">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="scrollMode === 'paged' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="scrollMode = 'paged'"
|
||||
>
|
||||
Paged
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="scrollMode === 'webtoon' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="scrollMode = 'webtoon'"
|
||||
>
|
||||
Webtoon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Reading direction</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Left-to-right for western; right-to-left for manga</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 p-1 rounded-lg border border-border bg-muted/50">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="readingDirection === 'ltr' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="readingDirection = 'ltr'"
|
||||
>
|
||||
L → R
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="readingDirection === 'rtl' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="readingDirection = 'rtl'"
|
||||
>
|
||||
R → L
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Fit mode</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">How pages are scaled to fit the screen</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
v-for="opt in [
|
||||
{ id: 'width', label: 'Width' },
|
||||
{ id: 'height', label: 'Height' },
|
||||
{ id: 'original', label: 'Original' },
|
||||
]"
|
||||
:key="opt.id"
|
||||
class="h-7 px-3 text-xs border-2 transition-colors font-medium rounded-md"
|
||||
:class="
|
||||
fitMode === opt.id
|
||||
? 'border-primary text-primary bg-primary/8'
|
||||
: 'border-border text-muted-foreground hover:border-muted-foreground/40 hover:text-foreground'
|
||||
"
|
||||
@click="fitMode = opt.id as 'width' | 'height' | 'original'"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Display</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Background color</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Canvas color behind pages</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
v-for="bg in backgrounds"
|
||||
:key="bg.id"
|
||||
class="h-7 px-3 text-xs border-2 transition-colors font-medium rounded-md"
|
||||
:class="
|
||||
background === bg.id
|
||||
? 'border-primary text-primary bg-primary/8'
|
||||
: 'border-border text-muted-foreground hover:border-muted-foreground/40 hover:text-foreground'
|
||||
"
|
||||
@click="background = bg.id as 'black' | 'white' | 'system'"
|
||||
>
|
||||
{{ bg.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Performance</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="px-5 py-4 bg-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Preload pages</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Number of pages to load ahead for faster navigation</p>
|
||||
</div>
|
||||
<span class="text-sm font-medium tabular-nums text-foreground">{{ preloadPages }}</span>
|
||||
</div>
|
||||
<input v-model.number="preloadPages" type="range" min="1" max="10" step="1" class="w-full accent-primary cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Behavior -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Behavior</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Remember position</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Resume from the last page you were on</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="rememberPosition ? 'bg-primary' : 'bg-muted'"
|
||||
@click="rememberPosition = !rememberPosition"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="rememberPosition ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-xs text-muted-foreground">These defaults will be persisted and applied to new comics in a future update.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const defaultFlow = ref<'paginated' | 'scrolled'>('paginated')
|
||||
const defaultFontSize = ref(18)
|
||||
const justify = ref(true)
|
||||
const hyphenate = ref(false)
|
||||
const rememberPosition = ref(true)
|
||||
const defaultTheme = ref('default')
|
||||
|
||||
const themes = [
|
||||
{ id: 'default', label: 'Default' },
|
||||
{ id: 'sepia', label: 'Sepia' },
|
||||
{ id: 'dark', label: 'Dark' },
|
||||
{ id: 'night', label: 'Night' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">eBook Reader</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default settings applied when opening EPUB and MOBI files.</p>
|
||||
</div>
|
||||
|
||||
<!-- Layout -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Layout</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Reading flow</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Paginated flips pages; scrolled flows continuously</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 p-1 rounded-lg border border-border bg-muted/50">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="defaultFlow === 'paginated' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="defaultFlow = 'paginated'"
|
||||
>
|
||||
Paginated
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="defaultFlow === 'scrolled' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="defaultFlow = 'scrolled'"
|
||||
>
|
||||
Scrolled
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Default theme</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Starting color scheme for new books</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
v-for="t in themes"
|
||||
:key="t.id"
|
||||
class="h-7 px-3 text-xs border-2 transition-colors font-medium rounded-md"
|
||||
:class="
|
||||
defaultTheme === t.id
|
||||
? 'border-primary text-primary bg-primary/8'
|
||||
: 'border-border text-muted-foreground hover:border-muted-foreground/40 hover:text-foreground'
|
||||
"
|
||||
@click="defaultTheme = t.id"
|
||||
>
|
||||
{{ t.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Typography -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Typography</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="px-5 py-4 bg-card">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Default font size</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Base size before any reader adjustment</p>
|
||||
</div>
|
||||
<span class="text-sm font-medium tabular-nums text-foreground">{{ defaultFontSize }}px</span>
|
||||
</div>
|
||||
<input v-model.number="defaultFontSize" type="range" min="10" max="32" step="1" class="w-full accent-primary cursor-pointer" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Justify text</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Align text to both margins</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="justify ? 'bg-primary' : 'bg-muted'"
|
||||
@click="justify = !justify"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="justify ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Hyphenation</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Auto word-break with hyphens</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="hyphenate ? 'bg-primary' : 'bg-muted'"
|
||||
@click="hyphenate = !hyphenate"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="hyphenate ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Behavior -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Behavior</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Remember position</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Resume from where you left off</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="rememberPosition ? 'bg-primary' : 'bg-muted'"
|
||||
@click="rememberPosition = !rememberPosition"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="rememberPosition ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-xs text-muted-foreground">These defaults will be persisted and applied to new books in a future update.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { FolderOpen, Plus, RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
interface Library {
|
||||
id: number
|
||||
name: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const libraries = ref<Library[]>([])
|
||||
const scanning = ref<Record<number, boolean>>({})
|
||||
const scanningAll = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await fetch('/api/libraries')
|
||||
if (res.ok) libraries.value = await res.json()
|
||||
})
|
||||
|
||||
async function scan(lib: Library) {
|
||||
scanning.value[lib.id] = true
|
||||
try {
|
||||
await fetch(`/api/scanner/libraries/${lib.id}/scan`, { method: 'POST' })
|
||||
} finally {
|
||||
scanning.value[lib.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function scanAll() {
|
||||
scanningAll.value = true
|
||||
try {
|
||||
await Promise.all(libraries.value.map((lib) => fetch(`/api/scanner/libraries/${lib.id}/scan`, { method: 'POST' })))
|
||||
} finally {
|
||||
scanningAll.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between mb-8">
|
||||
<div>
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Libraries</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Manage your media libraries and trigger content scans.</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0 ml-4">
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors text-muted-foreground hover:text-foreground disabled:opacity-50"
|
||||
:disabled="scanningAll || libraries.length === 0"
|
||||
@click="scanAll"
|
||||
>
|
||||
<RefreshCw :size="12" :class="scanningAll ? 'animate-spin' : ''" />
|
||||
{{ scanningAll ? 'Scanning…' : 'Scan All' }}
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md bg-primary text-primary-foreground hover:opacity-90 transition-opacity opacity-50 cursor-not-allowed"
|
||||
disabled
|
||||
title="Coming soon"
|
||||
>
|
||||
<Plus :size="12" />
|
||||
Add Library
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Library list -->
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div v-for="lib in libraries" :key="lib.id" class="bg-card px-5 py-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||
<FolderOpen :size="17" class="text-primary" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm font-medium text-foreground truncate">{{ lib.name }}</p>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
Added {{ new Date(lib.createdAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors text-muted-foreground hover:text-foreground disabled:opacity-50 shrink-0"
|
||||
:disabled="scanning[lib.id]"
|
||||
@click="scan(lib)"
|
||||
>
|
||||
<RefreshCw :size="12" :class="scanning[lib.id] ? 'animate-spin' : ''" />
|
||||
{{ scanning[lib.id] ? 'Scanning…' : 'Scan' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="libraries.length === 0" class="bg-card px-5 py-12 text-center">
|
||||
<FolderOpen :size="28" class="text-muted-foreground/30 mx-auto mb-3" />
|
||||
<p class="text-sm text-muted-foreground">No libraries configured yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info note -->
|
||||
<div class="mt-6 rounded-lg border border-border bg-muted/40 px-5 py-4">
|
||||
<p class="text-xs font-medium text-foreground mb-1">Library folders</p>
|
||||
<p class="text-xs text-muted-foreground leading-relaxed">
|
||||
Libraries are configured via the server environment and database. Folder path management and adding new libraries from the UI will be
|
||||
available in a future update.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const defaultZoom = ref('fit-width')
|
||||
const defaultScroll = ref<'continuous' | 'page'>('continuous')
|
||||
const defaultSpread = ref<'none' | 'odd'>('none')
|
||||
const invertColors = ref(false)
|
||||
const rememberPosition = ref(true)
|
||||
|
||||
const zoomOptions = [
|
||||
{ id: 'fit-width', label: 'Fit Width' },
|
||||
{ id: 'fit-page', label: 'Fit Page' },
|
||||
{ id: '0.75', label: '75%' },
|
||||
{ id: '1', label: '100%' },
|
||||
{ id: '1.25', label: '125%' },
|
||||
{ id: '1.5', label: '150%' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">PDF Reader</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default settings applied when opening PDF files.</p>
|
||||
</div>
|
||||
|
||||
<!-- Zoom -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Zoom</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="px-5 py-4 bg-card">
|
||||
<div class="mb-3">
|
||||
<p class="text-sm font-medium text-foreground">Default zoom level</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Starting zoom when a PDF is opened</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="opt in zoomOptions"
|
||||
:key="opt.id"
|
||||
class="h-8 px-3 text-xs border-2 transition-colors font-medium rounded-md"
|
||||
:class="
|
||||
defaultZoom === opt.id
|
||||
? 'border-primary text-primary bg-primary/8'
|
||||
: 'border-border text-muted-foreground hover:border-muted-foreground/40 hover:text-foreground'
|
||||
"
|
||||
@click="defaultZoom = opt.id"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Layout -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Layout</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Scroll mode</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Continuous scrolls through pages; Page by Page flips one at a time</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 p-1 rounded-lg border border-border bg-muted/50">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="defaultScroll === 'continuous' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="defaultScroll = 'continuous'"
|
||||
>
|
||||
Continuous
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="defaultScroll === 'page' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="defaultScroll = 'page'"
|
||||
>
|
||||
Page by Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Page spread</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Show one or two pages side by side</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 p-1 rounded-lg border border-border bg-muted/50">
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="defaultSpread === 'none' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="defaultSpread = 'none'"
|
||||
>
|
||||
Single
|
||||
</button>
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
||||
:class="defaultSpread === 'odd' ? 'bg-background shadow-sm text-foreground' : 'text-muted-foreground hover:text-foreground'"
|
||||
@click="defaultSpread = 'odd'"
|
||||
>
|
||||
Two-Page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Display</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Invert colors</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Invert PDF colors for easier reading in dark mode</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="invertColors ? 'bg-primary' : 'bg-muted'"
|
||||
@click="invertColors = !invertColors"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="invertColors ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Behavior -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Behavior</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Remember position</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Resume from the last page you were on</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="rememberPosition ? 'bg-primary' : 'bg-muted'"
|
||||
@click="rememberPosition = !rememberPosition"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="rememberPosition ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-xs text-muted-foreground">These defaults will be persisted and applied to new documents in a future update.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const defaultFlow = ref('paginated')
|
||||
const rememberPosition = ref(true)
|
||||
const preloadPages = ref(true)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto px-10 py-10">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Reading</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Default behavior and preferences for the reader.</p>
|
||||
</div>
|
||||
|
||||
<div class="border border-border rounded-lg overflow-hidden">
|
||||
<!-- Remember position -->
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card border-b border-border">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Remember position</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Resume from where you left off</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="rememberPosition ? 'bg-primary' : 'bg-muted'"
|
||||
@click="rememberPosition = !rememberPosition"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="rememberPosition ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Preload pages -->
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card border-b border-border">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Preload pages</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Load upcoming pages in the background (comic reader)</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="preloadPages ? 'bg-primary' : 'bg-muted'"
|
||||
@click="preloadPages = !preloadPages"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="preloadPages ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Default comic flow -->
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Default comic layout</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Starting scroll mode for CBZ / CBR / CB7</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<button
|
||||
v-for="opt in [
|
||||
{ value: 'paginated', label: 'Paginated' },
|
||||
{ value: 'infinite', label: 'Infinite' },
|
||||
{ value: 'long-strip', label: 'Strip' },
|
||||
]"
|
||||
:key="opt.value"
|
||||
class="h-7 px-3 text-xs rounded-md border-2 transition-colors"
|
||||
:class="
|
||||
defaultFlow === opt.value
|
||||
? 'border-primary text-primary bg-primary/8'
|
||||
: 'border-border text-muted-foreground hover:border-muted-foreground/40 hover:text-foreground'
|
||||
"
|
||||
@click="defaultFlow = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-xs text-muted-foreground">Reader preferences will be persisted in a future update.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const autoScan = ref(true)
|
||||
const scanOnStartup = ref(true)
|
||||
const watchForChanges = ref(false)
|
||||
const scanInterval = ref('hourly')
|
||||
|
||||
const fileTypes = ref([
|
||||
{ ext: 'epub', label: 'EPUB', enabled: true },
|
||||
{ ext: 'pdf', label: 'PDF', enabled: true },
|
||||
{ ext: 'cbz', label: 'CBZ', enabled: true },
|
||||
{ ext: 'cbr', label: 'CBR', enabled: true },
|
||||
{ ext: 'cb7', label: 'CB7', enabled: true },
|
||||
{ ext: 'mobi', label: 'MOBI', enabled: true },
|
||||
{ ext: 'azw3', label: 'AZW3', enabled: true },
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="px-10 py-8 max-w-3xl">
|
||||
<div class="mb-8">
|
||||
<h2 class="font-serif font-semibold text-foreground text-2xl tracking-tight">Scanner</h2>
|
||||
<p class="mt-1 text-sm text-muted-foreground">Configure when and how your libraries are scanned for new content.</p>
|
||||
</div>
|
||||
|
||||
<!-- Schedule -->
|
||||
<div class="mb-6">
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Schedule</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Automatic scanning</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Periodically scan libraries for new or changed files</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="autoScan ? 'bg-primary' : 'bg-muted'"
|
||||
@click="autoScan = !autoScan"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="autoScan ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card" :class="!autoScan ? 'opacity-40 pointer-events-none' : ''">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Scan interval</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">How often to check all library folders</p>
|
||||
</div>
|
||||
<select
|
||||
v-model="scanInterval"
|
||||
class="text-sm border border-border rounded-md px-3 py-1.5 bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="15min">Every 15 minutes</option>
|
||||
<option value="hourly">Every hour</option>
|
||||
<option value="6h">Every 6 hours</option>
|
||||
<option value="daily">Once a day</option>
|
||||
<option value="weekly">Once a week</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Scan on startup</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Run a scan automatically when the server starts</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="scanOnStartup ? 'bg-primary' : 'bg-muted'"
|
||||
@click="scanOnStartup = !scanOnStartup"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="scanOnStartup ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between px-5 py-4 bg-card">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-foreground">Watch for changes</p>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">Detect new files instantly using filesystem events</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-11 h-6 rounded-full transition-colors relative shrink-0"
|
||||
:class="watchForChanges ? 'bg-primary' : 'bg-muted'"
|
||||
@click="watchForChanges = !watchForChanges"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="watchForChanges ? 'translate-x-6' : 'translate-x-1'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File types -->
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Supported file types</p>
|
||||
<div class="border border-border rounded-lg overflow-hidden divide-y divide-border">
|
||||
<div v-for="type in fileTypes" :key="type.ext" class="flex items-center justify-between px-5 py-3.5 bg-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs font-mono font-medium text-muted-foreground bg-muted px-1.5 py-0.5 rounded uppercase"> .{{ type.ext }} </span>
|
||||
<p class="text-sm text-foreground">{{ type.label }}</p>
|
||||
</div>
|
||||
<button
|
||||
class="w-9 h-5 rounded-full transition-colors relative shrink-0"
|
||||
:class="type.enabled ? 'bg-primary' : 'bg-muted'"
|
||||
@click="type.enabled = !type.enabled"
|
||||
>
|
||||
<div
|
||||
class="absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform shadow-sm"
|
||||
:class="type.enabled ? 'translate-x-4' : 'translate-x-0.5'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 text-xs text-muted-foreground">Scanner configuration will be persisted in a future update.</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { X, Library, ScanLine, Paintbrush, BookOpen, FileText, BookImage, Info, ChevronLeft } from 'lucide-vue-next'
|
||||
import { useSettingsDrawer } from '@/composables/useSettingsDrawer'
|
||||
import LibrariesSettings from './LibrariesSettings.vue'
|
||||
import ScannerSettings from './ScannerSettings.vue'
|
||||
import AppearanceSettings from './AppearanceSettings.vue'
|
||||
import EbookSettings from './EbookSettings.vue'
|
||||
import PdfSettings from './PdfSettings.vue'
|
||||
import ComicsSettings from './ComicsSettings.vue'
|
||||
import AboutSettings from './AboutSettings.vue'
|
||||
|
||||
const { isOpen, close } = useSettingsDrawer()
|
||||
|
||||
type SectionId = 'libraries' | 'scanner' | 'appearance' | 'ebook' | 'pdf' | 'comics' | 'about'
|
||||
|
||||
const navGroups = [
|
||||
{
|
||||
label: 'General',
|
||||
items: [
|
||||
{ id: 'libraries' as SectionId, label: 'Libraries', icon: Library, component: LibrariesSettings },
|
||||
{ id: 'scanner' as SectionId, label: 'Scanner', icon: ScanLine, component: ScannerSettings },
|
||||
{ id: 'appearance' as SectionId, label: 'Appearance', icon: Paintbrush, component: AppearanceSettings },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reader',
|
||||
items: [
|
||||
{ id: 'ebook' as SectionId, label: 'eBook', icon: BookOpen, component: EbookSettings },
|
||||
{ id: 'pdf' as SectionId, label: 'PDF', icon: FileText, component: PdfSettings },
|
||||
{ id: 'comics' as SectionId, label: 'Comics', icon: BookImage, component: ComicsSettings },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'System',
|
||||
items: [{ id: 'about' as SectionId, label: 'About', icon: Info, component: AboutSettings }],
|
||||
},
|
||||
]
|
||||
|
||||
const allItems = navGroups.flatMap((g) => g.items)
|
||||
const activeId = ref<SectionId>('libraries')
|
||||
const ActiveComponent = shallowRef(LibrariesSettings)
|
||||
|
||||
function navigate(id: SectionId) {
|
||||
activeId.value = id
|
||||
ActiveComponent.value = allItems.find((i) => i.id === id)!.component
|
||||
}
|
||||
|
||||
watch(isOpen, (v) => {
|
||||
if (v) navigate('libraries')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="drawer-fade">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex justify-end" @click.self="close">
|
||||
<!-- Backdrop -->
|
||||
<div class="absolute inset-0 bg-black/40 backdrop-blur-[2px]" @click="close" />
|
||||
|
||||
<!-- Panel -->
|
||||
<Transition name="drawer-slide">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="relative flex h-full w-full max-w-[900px] shadow-2xl overflow-hidden"
|
||||
style="border-left: 1px solid var(--border)"
|
||||
>
|
||||
<!-- Left nav -->
|
||||
<nav class="flex flex-col w-52 shrink-0 bg-muted/40 border-r border-border h-full">
|
||||
<div class="px-4 pt-5 pb-4 border-b border-border flex items-center justify-between">
|
||||
<span class="text-sm font-semibold text-foreground font-serif">Settings</span>
|
||||
<button
|
||||
class="flex items-center justify-center w-6 h-6 rounded text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
<X :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto py-3 px-2">
|
||||
<div v-for="group in navGroups" :key="group.label" class="mb-4">
|
||||
<p class="px-2 mb-1 text-[10px] font-semibold text-muted-foreground/60 uppercase tracking-widest">
|
||||
{{ group.label }}
|
||||
</p>
|
||||
<button
|
||||
v-for="item in group.items"
|
||||
:key="item.id"
|
||||
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-lg text-sm transition-colors relative"
|
||||
:class="
|
||||
activeId === item.id
|
||||
? 'bg-background text-foreground font-medium shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/60'
|
||||
"
|
||||
@click="navigate(item.id)"
|
||||
>
|
||||
<component :is="item.icon" :size="14" :class="activeId === item.id ? 'text-primary' : 'text-muted-foreground/70'" />
|
||||
{{ item.label }}
|
||||
<div v-if="activeId === item.id" class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 rounded-r bg-primary" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-3 border-t border-border">
|
||||
<p class="text-[10px] text-muted-foreground/40">projectx · v0.1.0</p>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Content -->
|
||||
<main class="flex-1 overflow-y-auto bg-background">
|
||||
<component :is="ActiveComponent" />
|
||||
</main>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.drawer-fade-enter-active,
|
||||
.drawer-fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.drawer-fade-enter-from,
|
||||
.drawer-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.drawer-slide-enter-active,
|
||||
.drawer-slide-leave-active {
|
||||
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.drawer-slide-enter-from,
|
||||
.drawer-slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import BookCoverCard from '@/features/book/components/BookCoverCard.vue'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import AppSidebar from '@/components/AppSidebar.vue'
|
||||
import SettingsDrawer from '@/features/settings/SettingsDrawer.vue'
|
||||
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar'
|
||||
import { useBooks } from '@/features/book/composables/useBooks'
|
||||
import { useDisplaySettings } from '@/composables/useDisplaySettings'
|
||||
@@ -44,6 +45,7 @@ watch(search, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SettingsDrawer />
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1767510287823,
|
||||
"when": 1767748212922,
|
||||
"tag": "0000_init_schema",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1767510287823,
|
||||
"when": 1767748212922,
|
||||
"tag": "0001_lethal_pixie",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1767510287823,
|
||||
"when": 1767748212922,
|
||||
"tag": "0002_powerful_thundra",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1767510287823,
|
||||
"when": 1767748212922,
|
||||
"tag": "0003_reading_progress_by_file",
|
||||
"breakpoints": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user