Mayo commited on
feat: about page
Browse files- koharu/capabilities/default.json +8 -1
- koharu/tauri.conf.json +2 -1
- ui/app/(app)/about/page.tsx +160 -0
- ui/app/(app)/layout.tsx +12 -0
- ui/app/{page.tsx → (app)/page.tsx} +2 -4
- ui/app/(app)/settings/page.tsx +130 -0
- ui/app/settings/page.tsx +0 -62
- ui/components/MenuBar.tsx +115 -56
- ui/lib/backend.ts +28 -0
- ui/public/icon-large.png +3 -0
- ui/public/icon.png +3 -0
- ui/public/locales/en-US/translation.json +14 -1
- ui/public/locales/ja-JP/translation.json +14 -1
- ui/public/locales/zh-CN/translation.json +14 -1
- ui/public/locales/zh-TW/translation.json +14 -1
koharu/capabilities/default.json
CHANGED
|
@@ -8,6 +8,13 @@
|
|
| 8 |
],
|
| 9 |
"permissions": [
|
| 10 |
"core:default",
|
| 11 |
-
"core:window:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
]
|
| 13 |
}
|
|
|
|
| 8 |
],
|
| 9 |
"permissions": [
|
| 10 |
"core:default",
|
| 11 |
+
"core:window:default",
|
| 12 |
+
"core:window:allow-close",
|
| 13 |
+
"core:window:allow-set-progress-bar",
|
| 14 |
+
"core:window:allow-start-dragging",
|
| 15 |
+
"core:window:allow-minimize",
|
| 16 |
+
"core:window:allow-maximize",
|
| 17 |
+
"core:window:allow-toggle-maximize",
|
| 18 |
+
"core:window:allow-internal-toggle-maximize"
|
| 19 |
]
|
| 20 |
}
|
koharu/tauri.conf.json
CHANGED
|
@@ -18,7 +18,8 @@
|
|
| 18 |
"width": 1024,
|
| 19 |
"height": 768,
|
| 20 |
"center": true,
|
| 21 |
-
"title": "Koharu"
|
|
|
|
| 22 |
},
|
| 23 |
{
|
| 24 |
"label": "splashscreen",
|
|
|
|
| 18 |
"width": 1024,
|
| 19 |
"height": 768,
|
| 20 |
"center": true,
|
| 21 |
+
"title": "Koharu",
|
| 22 |
+
"decorations": false
|
| 23 |
},
|
| 24 |
{
|
| 25 |
"label": "splashscreen",
|
ui/app/(app)/about/page.tsx
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { useTranslation } from 'react-i18next'
|
| 5 |
+
import { useRouter } from 'next/navigation'
|
| 6 |
+
import {
|
| 7 |
+
ChevronLeftIcon,
|
| 8 |
+
CheckCircleIcon,
|
| 9 |
+
AlertCircleIcon,
|
| 10 |
+
LoaderIcon,
|
| 11 |
+
} from 'lucide-react'
|
| 12 |
+
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 13 |
+
import { invoke, isTauri } from '@/lib/backend'
|
| 14 |
+
import { useAppStore } from '@/lib/store'
|
| 15 |
+
import Image from 'next/image'
|
| 16 |
+
|
| 17 |
+
const GITHUB_REPO = 'mayocream/koharu'
|
| 18 |
+
|
| 19 |
+
type VersionStatus = 'loading' | 'latest' | 'outdated' | 'error'
|
| 20 |
+
|
| 21 |
+
export default function AboutPage() {
|
| 22 |
+
const { t } = useTranslation()
|
| 23 |
+
const router = useRouter()
|
| 24 |
+
const { openExternal } = useAppStore()
|
| 25 |
+
|
| 26 |
+
const [appVersion, setAppVersion] = useState<string>()
|
| 27 |
+
const [latestVersion, setLatestVersion] = useState<string>()
|
| 28 |
+
const [versionStatus, setVersionStatus] = useState<VersionStatus>('loading')
|
| 29 |
+
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
const checkVersion = async () => {
|
| 32 |
+
try {
|
| 33 |
+
if (isTauri()) {
|
| 34 |
+
const version = await invoke<string>('app_version')
|
| 35 |
+
setAppVersion(version)
|
| 36 |
+
|
| 37 |
+
const res = await fetch(
|
| 38 |
+
`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
|
| 39 |
+
)
|
| 40 |
+
if (res.ok) {
|
| 41 |
+
const data = await res.json()
|
| 42 |
+
const latest = data.tag_name?.replace(/^v/, '') || data.name
|
| 43 |
+
setLatestVersion(latest)
|
| 44 |
+
setVersionStatus(version === latest ? 'latest' : 'outdated')
|
| 45 |
+
} else {
|
| 46 |
+
setVersionStatus('error')
|
| 47 |
+
}
|
| 48 |
+
} else {
|
| 49 |
+
setAppVersion('dev')
|
| 50 |
+
setVersionStatus('latest')
|
| 51 |
+
}
|
| 52 |
+
} catch {
|
| 53 |
+
setVersionStatus('error')
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
void checkVersion()
|
| 58 |
+
}, [])
|
| 59 |
+
|
| 60 |
+
return (
|
| 61 |
+
<div className='bg-muted flex flex-1 flex-col overflow-hidden'>
|
| 62 |
+
<ScrollArea className='flex-1'>
|
| 63 |
+
<div className='px-4 py-6'>
|
| 64 |
+
<div className='relative mx-auto max-w-xl'>
|
| 65 |
+
{/* Header with back button */}
|
| 66 |
+
<div className='mb-8 flex items-center'>
|
| 67 |
+
<button
|
| 68 |
+
onClick={() => router.push('/')}
|
| 69 |
+
className='text-muted-foreground hover:bg-accent hover:text-foreground absolute -left-14 flex size-10 items-center justify-center rounded-full transition'
|
| 70 |
+
>
|
| 71 |
+
<ChevronLeftIcon className='size-6' />
|
| 72 |
+
</button>
|
| 73 |
+
<h1 className='text-foreground text-2xl font-bold'>
|
| 74 |
+
{t('settings.about')}
|
| 75 |
+
</h1>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
{/* App Info */}
|
| 79 |
+
<div className='mb-8 flex flex-col items-center text-center'>
|
| 80 |
+
<Image
|
| 81 |
+
src='/icon-large.png'
|
| 82 |
+
alt='Koharu'
|
| 83 |
+
width={96}
|
| 84 |
+
height={96}
|
| 85 |
+
className='mb-4'
|
| 86 |
+
draggable={false}
|
| 87 |
+
/>
|
| 88 |
+
<h2 className='text-foreground mb-1 text-xl font-bold'>Koharu</h2>
|
| 89 |
+
<p className='text-muted-foreground text-sm'>
|
| 90 |
+
{t('settings.aboutTagline')}
|
| 91 |
+
</p>
|
| 92 |
+
</div>
|
| 93 |
+
|
| 94 |
+
{/* Version & Info Card */}
|
| 95 |
+
<div className='bg-card border-border rounded-lg border p-4'>
|
| 96 |
+
<div className='space-y-3 text-sm'>
|
| 97 |
+
<div className='flex items-center justify-between'>
|
| 98 |
+
<span className='text-muted-foreground'>
|
| 99 |
+
{t('settings.aboutVersion')}
|
| 100 |
+
</span>
|
| 101 |
+
<div className='flex items-center gap-2'>
|
| 102 |
+
<span className='text-foreground font-medium'>
|
| 103 |
+
{appVersion || '...'}
|
| 104 |
+
</span>
|
| 105 |
+
{versionStatus === 'loading' && (
|
| 106 |
+
<LoaderIcon className='text-muted-foreground size-4 animate-spin' />
|
| 107 |
+
)}
|
| 108 |
+
{versionStatus === 'latest' && (
|
| 109 |
+
<span className='flex items-center gap-1 text-xs text-green-500'>
|
| 110 |
+
<CheckCircleIcon className='size-3.5' />
|
| 111 |
+
{t('settings.aboutLatest')}
|
| 112 |
+
</span>
|
| 113 |
+
)}
|
| 114 |
+
{versionStatus === 'outdated' && (
|
| 115 |
+
<button
|
| 116 |
+
onClick={() =>
|
| 117 |
+
openExternal(
|
| 118 |
+
`https://github.com/${GITHUB_REPO}/releases/latest`,
|
| 119 |
+
)
|
| 120 |
+
}
|
| 121 |
+
className='flex items-center gap-1 text-xs text-amber-500 hover:underline'
|
| 122 |
+
>
|
| 123 |
+
<AlertCircleIcon className='size-3.5' />
|
| 124 |
+
{t('settings.aboutUpdate', { version: latestVersion })}
|
| 125 |
+
</button>
|
| 126 |
+
)}
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
<div className='flex items-center justify-between'>
|
| 130 |
+
<span className='text-muted-foreground'>
|
| 131 |
+
{t('settings.aboutAuthor')}
|
| 132 |
+
</span>
|
| 133 |
+
<button
|
| 134 |
+
onClick={() => openExternal('https://github.com/mayocream')}
|
| 135 |
+
className='text-foreground font-medium hover:underline'
|
| 136 |
+
>
|
| 137 |
+
Mayo
|
| 138 |
+
</button>
|
| 139 |
+
</div>
|
| 140 |
+
<div className='flex items-center justify-between'>
|
| 141 |
+
<span className='text-muted-foreground'>
|
| 142 |
+
{t('settings.aboutRepository')}
|
| 143 |
+
</span>
|
| 144 |
+
<button
|
| 145 |
+
onClick={() =>
|
| 146 |
+
openExternal(`https://github.com/${GITHUB_REPO}`)
|
| 147 |
+
}
|
| 148 |
+
className='text-foreground font-medium hover:underline'
|
| 149 |
+
>
|
| 150 |
+
GitHub
|
| 151 |
+
</button>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
</ScrollArea>
|
| 158 |
+
</div>
|
| 159 |
+
)
|
| 160 |
+
}
|
ui/app/(app)/layout.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { MenuBar } from '@/components/MenuBar'
|
| 4 |
+
|
| 5 |
+
export default function AppLayout({ children }: { children: React.ReactNode }) {
|
| 6 |
+
return (
|
| 7 |
+
<div className='bg-background flex h-screen w-screen flex-col overflow-hidden'>
|
| 8 |
+
<MenuBar />
|
| 9 |
+
{children}
|
| 10 |
+
</div>
|
| 11 |
+
)
|
| 12 |
+
}
|
ui/app/{page.tsx → (app)/page.tsx}
RENAMED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { MenuBar } from '@/components/MenuBar'
|
| 4 |
import { Panels } from '@/components/Panels'
|
| 5 |
import { Workspace, StatusBar } from '@/components/Canvas'
|
| 6 |
import { Navigator } from '@/components/Navigator'
|
|
@@ -8,8 +7,7 @@ import { ActivityBubble } from '@/components/ActivityBubble'
|
|
| 8 |
|
| 9 |
export default function Page() {
|
| 10 |
return (
|
| 11 |
-
<
|
| 12 |
-
<MenuBar />
|
| 13 |
<ActivityBubble />
|
| 14 |
<div className='flex min-h-0 flex-1'>
|
| 15 |
<Navigator />
|
|
@@ -19,6 +17,6 @@ export default function Page() {
|
|
| 19 |
</div>
|
| 20 |
<Panels />
|
| 21 |
</div>
|
| 22 |
-
</
|
| 23 |
)
|
| 24 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
|
|
|
| 3 |
import { Panels } from '@/components/Panels'
|
| 4 |
import { Workspace, StatusBar } from '@/components/Canvas'
|
| 5 |
import { Navigator } from '@/components/Navigator'
|
|
|
|
| 7 |
|
| 8 |
export default function Page() {
|
| 9 |
return (
|
| 10 |
+
<div className='flex flex-1 flex-col'>
|
|
|
|
| 11 |
<ActivityBubble />
|
| 12 |
<div className='flex min-h-0 flex-1'>
|
| 13 |
<Navigator />
|
|
|
|
| 17 |
</div>
|
| 18 |
<Panels />
|
| 19 |
</div>
|
| 20 |
+
</div>
|
| 21 |
)
|
| 22 |
}
|
ui/app/(app)/settings/page.tsx
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useMemo } from 'react'
|
| 4 |
+
import { useTheme } from 'next-themes'
|
| 5 |
+
import { useTranslation } from 'react-i18next'
|
| 6 |
+
import { useRouter } from 'next/navigation'
|
| 7 |
+
import {
|
| 8 |
+
SunIcon,
|
| 9 |
+
MoonIcon,
|
| 10 |
+
MonitorIcon,
|
| 11 |
+
ChevronLeftIcon,
|
| 12 |
+
ChevronRightIcon,
|
| 13 |
+
} from 'lucide-react'
|
| 14 |
+
import { ScrollArea } from '@/components/ui/scroll-area'
|
| 15 |
+
import {
|
| 16 |
+
Select,
|
| 17 |
+
SelectContent,
|
| 18 |
+
SelectItem,
|
| 19 |
+
SelectTrigger,
|
| 20 |
+
SelectValue,
|
| 21 |
+
} from '@/components/ui/select'
|
| 22 |
+
|
| 23 |
+
const THEME_OPTIONS = [
|
| 24 |
+
{ value: 'light', icon: SunIcon, labelKey: 'settings.themeLight' },
|
| 25 |
+
{ value: 'dark', icon: MoonIcon, labelKey: 'settings.themeDark' },
|
| 26 |
+
{ value: 'system', icon: MonitorIcon, labelKey: 'settings.themeSystem' },
|
| 27 |
+
] as const
|
| 28 |
+
|
| 29 |
+
export default function SettingsPage() {
|
| 30 |
+
const { t, i18n } = useTranslation()
|
| 31 |
+
const { theme, setTheme } = useTheme()
|
| 32 |
+
const router = useRouter()
|
| 33 |
+
const locales = useMemo(
|
| 34 |
+
() => Object.keys(i18n.options.resources || {}),
|
| 35 |
+
[i18n.options.resources],
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
return (
|
| 39 |
+
<div className='bg-muted flex flex-1 flex-col overflow-hidden'>
|
| 40 |
+
<ScrollArea className='flex-1'>
|
| 41 |
+
<div className='px-4 py-6'>
|
| 42 |
+
{/* Content column */}
|
| 43 |
+
<div className='relative mx-auto max-w-xl'>
|
| 44 |
+
{/* Header with back button */}
|
| 45 |
+
<div className='mb-8 flex items-center'>
|
| 46 |
+
<button
|
| 47 |
+
onClick={() => router.push('/')}
|
| 48 |
+
className='text-muted-foreground hover:bg-accent hover:text-foreground absolute -left-14 flex size-10 items-center justify-center rounded-full transition'
|
| 49 |
+
>
|
| 50 |
+
<ChevronLeftIcon className='size-6' />
|
| 51 |
+
</button>
|
| 52 |
+
<h1 className='text-foreground text-2xl font-bold'>
|
| 53 |
+
{t('settings.title')}
|
| 54 |
+
</h1>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
{/* Appearance Section */}
|
| 58 |
+
<section className='mb-8'>
|
| 59 |
+
<h2 className='text-foreground mb-1 text-sm font-bold'>
|
| 60 |
+
{t('settings.appearance')}
|
| 61 |
+
</h2>
|
| 62 |
+
<p className='text-muted-foreground mb-4 text-sm'>
|
| 63 |
+
{t('settings.appearanceDescription')}
|
| 64 |
+
</p>
|
| 65 |
+
|
| 66 |
+
<div className='space-y-3'>
|
| 67 |
+
<div className='text-foreground text-sm'>
|
| 68 |
+
{t('settings.theme')}
|
| 69 |
+
</div>
|
| 70 |
+
<div className='flex gap-2'>
|
| 71 |
+
{THEME_OPTIONS.map(({ value, icon: Icon, labelKey }) => (
|
| 72 |
+
<button
|
| 73 |
+
key={value}
|
| 74 |
+
onClick={() => setTheme(value)}
|
| 75 |
+
data-active={theme === value}
|
| 76 |
+
className='border-border bg-card text-muted-foreground hover:border-foreground/30 data-[active=true]:border-primary data-[active=true]:text-foreground flex flex-1 flex-col items-center gap-2 rounded-lg border p-3 transition'
|
| 77 |
+
>
|
| 78 |
+
<Icon className='size-5' />
|
| 79 |
+
<span className='text-xs font-medium'>{t(labelKey)}</span>
|
| 80 |
+
</button>
|
| 81 |
+
))}
|
| 82 |
+
</div>
|
| 83 |
+
</div>
|
| 84 |
+
</section>
|
| 85 |
+
|
| 86 |
+
{/* Language Section */}
|
| 87 |
+
<section className='mb-8'>
|
| 88 |
+
<h2 className='text-foreground mb-1 text-sm font-bold'>
|
| 89 |
+
{t('settings.language')}
|
| 90 |
+
</h2>
|
| 91 |
+
<p className='text-muted-foreground mb-4 text-sm'>
|
| 92 |
+
{t('settings.languageDescription')}
|
| 93 |
+
</p>
|
| 94 |
+
|
| 95 |
+
<Select
|
| 96 |
+
value={i18n.language}
|
| 97 |
+
onValueChange={(value) => i18n.changeLanguage(value)}
|
| 98 |
+
>
|
| 99 |
+
<SelectTrigger className='w-full'>
|
| 100 |
+
<SelectValue />
|
| 101 |
+
</SelectTrigger>
|
| 102 |
+
<SelectContent>
|
| 103 |
+
{locales.map((code) => (
|
| 104 |
+
<SelectItem key={code} value={code}>
|
| 105 |
+
{t(`menu.languages.${code}`)}
|
| 106 |
+
</SelectItem>
|
| 107 |
+
))}
|
| 108 |
+
</SelectContent>
|
| 109 |
+
</Select>
|
| 110 |
+
</section>
|
| 111 |
+
|
| 112 |
+
{/* Divider */}
|
| 113 |
+
<div className='border-border mb-8 border-t' />
|
| 114 |
+
|
| 115 |
+
{/* About Link */}
|
| 116 |
+
<button
|
| 117 |
+
onClick={() => router.push('/about')}
|
| 118 |
+
className='hover:bg-accent flex w-full items-center justify-between rounded-lg px-3 py-3 text-left transition'
|
| 119 |
+
>
|
| 120 |
+
<span className='text-foreground text-sm font-medium'>
|
| 121 |
+
{t('settings.about')}
|
| 122 |
+
</span>
|
| 123 |
+
<ChevronRightIcon className='text-muted-foreground size-5' />
|
| 124 |
+
</button>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</ScrollArea>
|
| 128 |
+
</div>
|
| 129 |
+
)
|
| 130 |
+
}
|
ui/app/settings/page.tsx
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import { useTheme } from 'next-themes'
|
| 4 |
-
import { useTranslation } from 'react-i18next'
|
| 5 |
-
import { useRouter } from 'next/navigation'
|
| 6 |
-
import { ArrowLeftIcon, SunIcon, MoonIcon, MonitorIcon } from 'lucide-react'
|
| 7 |
-
import { Button } from '@/components/ui/button'
|
| 8 |
-
|
| 9 |
-
const THEME_OPTIONS = [
|
| 10 |
-
{ value: 'light', icon: SunIcon, labelKey: 'settings.themeLight' },
|
| 11 |
-
{ value: 'dark', icon: MoonIcon, labelKey: 'settings.themeDark' },
|
| 12 |
-
{ value: 'system', icon: MonitorIcon, labelKey: 'settings.themeSystem' },
|
| 13 |
-
] as const
|
| 14 |
-
|
| 15 |
-
export default function SettingsPage() {
|
| 16 |
-
const { t } = useTranslation()
|
| 17 |
-
const router = useRouter()
|
| 18 |
-
const { theme, setTheme } = useTheme()
|
| 19 |
-
|
| 20 |
-
return (
|
| 21 |
-
<main className='bg-muted flex h-screen w-screen flex-col'>
|
| 22 |
-
<header className='border-border bg-background flex h-12 items-center gap-3 border-b px-3'>
|
| 23 |
-
<Button
|
| 24 |
-
variant='ghost'
|
| 25 |
-
size='icon'
|
| 26 |
-
className='size-8'
|
| 27 |
-
onClick={() => router.back()}
|
| 28 |
-
>
|
| 29 |
-
<ArrowLeftIcon className='size-4' />
|
| 30 |
-
</Button>
|
| 31 |
-
<h1 className='text-foreground text-sm font-semibold'>
|
| 32 |
-
{t('settings.title')}
|
| 33 |
-
</h1>
|
| 34 |
-
</header>
|
| 35 |
-
|
| 36 |
-
<div className='flex-1 overflow-auto'>
|
| 37 |
-
<div className='p-4'>
|
| 38 |
-
<div className='space-y-4'>
|
| 39 |
-
<div className='space-y-3'>
|
| 40 |
-
<div className='text-muted-foreground text-xs font-semibold tracking-wide uppercase'>
|
| 41 |
-
{t('settings.theme')}
|
| 42 |
-
</div>
|
| 43 |
-
<div className='grid grid-cols-3 gap-3'>
|
| 44 |
-
{THEME_OPTIONS.map(({ value, icon: Icon, labelKey }) => (
|
| 45 |
-
<button
|
| 46 |
-
key={value}
|
| 47 |
-
onClick={() => setTheme(value)}
|
| 48 |
-
data-active={theme === value}
|
| 49 |
-
className='border-border bg-card text-muted-foreground hover:bg-accent data-[active=true]:border-primary data-[active=true]:bg-accent data-[active=true]:text-accent-foreground flex flex-col items-center gap-2 rounded-lg border p-4 transition'
|
| 50 |
-
>
|
| 51 |
-
<Icon className='size-6' />
|
| 52 |
-
<span className='text-xs font-medium'>{t(labelKey)}</span>
|
| 53 |
-
</button>
|
| 54 |
-
))}
|
| 55 |
-
</div>
|
| 56 |
-
</div>
|
| 57 |
-
</div>
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
</main>
|
| 61 |
-
)
|
| 62 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/components/MenuBar.tsx
CHANGED
|
@@ -1,18 +1,18 @@
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
-
import { useEffect, useState,
|
| 4 |
import { useRouter } from 'next/navigation'
|
| 5 |
-
import {
|
| 6 |
import { useTranslation } from 'react-i18next'
|
| 7 |
import { useAppStore } from '@/lib/store'
|
| 8 |
import { fitCanvasToViewport, resetCanvasScale } from '@/components/Canvas'
|
|
|
|
|
|
|
| 9 |
import {
|
| 10 |
Menubar,
|
| 11 |
MenubarContent,
|
| 12 |
MenubarItem,
|
| 13 |
MenubarMenu,
|
| 14 |
-
MenubarRadioGroup,
|
| 15 |
-
MenubarRadioItem,
|
| 16 |
MenubarSeparator,
|
| 17 |
MenubarTrigger,
|
| 18 |
} from '@/components/ui/menubar'
|
|
@@ -24,13 +24,9 @@ type MenuItem = {
|
|
| 24 |
}
|
| 25 |
|
| 26 |
export function MenuBar() {
|
| 27 |
-
const { t
|
| 28 |
const router = useRouter()
|
| 29 |
-
const
|
| 30 |
-
() => Object.keys(i18n.options.resources || {}),
|
| 31 |
-
[i18n.options.resources],
|
| 32 |
-
)
|
| 33 |
-
const [appVersion, setAppVersion] = useState<string>()
|
| 34 |
const {
|
| 35 |
openDocuments,
|
| 36 |
openExternal,
|
|
@@ -40,19 +36,33 @@ export function MenuBar() {
|
|
| 40 |
exportDocument,
|
| 41 |
saveDocuments,
|
| 42 |
} = useAppStore()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
useEffect(() => {
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
| 52 |
}
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
}, [])
|
| 56 |
|
| 57 |
const fileMenuItems: MenuItem[] = [
|
| 58 |
{ label: t('menu.openFile'), onSelect: openDocuments },
|
|
@@ -76,30 +86,34 @@ export function MenuBar() {
|
|
| 76 |
{ label: t('menu.processAll'), onSelect: processAllImages },
|
| 77 |
],
|
| 78 |
},
|
|
|
|
|
|
|
|
|
|
| 79 |
{
|
| 80 |
-
label: t('menu.
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
disabled: true,
|
| 87 |
-
},
|
| 88 |
-
{
|
| 89 |
-
label: t('menu.discord'),
|
| 90 |
-
onSelect: () => openExternal('https://discord.gg/mHvHkxGnUY'),
|
| 91 |
-
},
|
| 92 |
-
{
|
| 93 |
-
label: t('menu.github'),
|
| 94 |
-
onSelect: () => openExternal('https://github.com/mayocream/koharu'),
|
| 95 |
-
},
|
| 96 |
-
],
|
| 97 |
},
|
| 98 |
]
|
| 99 |
|
| 100 |
return (
|
| 101 |
-
<div className='border-border bg-background text-foreground flex h-8 items-center
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
<MenubarMenu>
|
| 104 |
<MenubarTrigger className='hover:bg-accent data-[state=open]:bg-accent rounded px-3 py-1.5 font-medium'>
|
| 105 |
{t('menu.file')}
|
|
@@ -167,7 +181,7 @@ export function MenuBar() {
|
|
| 167 |
))}
|
| 168 |
<MenubarMenu>
|
| 169 |
<MenubarTrigger className='hover:bg-accent data-[state=open]:bg-accent rounded px-3 py-1.5 font-medium'>
|
| 170 |
-
{t('menu.
|
| 171 |
</MenubarTrigger>
|
| 172 |
<MenubarContent
|
| 173 |
className='min-w-36'
|
|
@@ -175,27 +189,72 @@ export function MenuBar() {
|
|
| 175 |
sideOffset={5}
|
| 176 |
alignOffset={-3}
|
| 177 |
>
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
}
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
>
|
| 186 |
-
{
|
| 187 |
-
|
| 188 |
-
key={code}
|
| 189 |
-
value={code}
|
| 190 |
-
className='text-[13px]'
|
| 191 |
-
>
|
| 192 |
-
{t(`menu.languages.${code}`)}
|
| 193 |
-
</MenubarRadioItem>
|
| 194 |
-
))}
|
| 195 |
-
</MenubarRadioGroup>
|
| 196 |
</MenubarContent>
|
| 197 |
</MenubarMenu>
|
| 198 |
</Menubar>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
</div>
|
| 200 |
)
|
| 201 |
}
|
|
|
|
| 1 |
'use client'
|
| 2 |
|
| 3 |
+
import { useEffect, useState, useCallback } from 'react'
|
| 4 |
import { useRouter } from 'next/navigation'
|
| 5 |
+
import { windowControls, isTauri, listen } from '@/lib/backend'
|
| 6 |
import { useTranslation } from 'react-i18next'
|
| 7 |
import { useAppStore } from '@/lib/store'
|
| 8 |
import { fitCanvasToViewport, resetCanvasScale } from '@/components/Canvas'
|
| 9 |
+
import { Minus, Square, X, Copy } from 'lucide-react'
|
| 10 |
+
import Image from 'next/image'
|
| 11 |
import {
|
| 12 |
Menubar,
|
| 13 |
MenubarContent,
|
| 14 |
MenubarItem,
|
| 15 |
MenubarMenu,
|
|
|
|
|
|
|
| 16 |
MenubarSeparator,
|
| 17 |
MenubarTrigger,
|
| 18 |
} from '@/components/ui/menubar'
|
|
|
|
| 24 |
}
|
| 25 |
|
| 26 |
export function MenuBar() {
|
| 27 |
+
const { t } = useTranslation()
|
| 28 |
const router = useRouter()
|
| 29 |
+
const [isMaximized, setIsMaximized] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
const {
|
| 31 |
openDocuments,
|
| 32 |
openExternal,
|
|
|
|
| 36 |
exportDocument,
|
| 37 |
saveDocuments,
|
| 38 |
} = useAppStore()
|
| 39 |
+
|
| 40 |
+
const updateMaximizedState = useCallback(async () => {
|
| 41 |
+
const maximized = await windowControls.isMaximized()
|
| 42 |
+
setIsMaximized(maximized)
|
| 43 |
+
}, [])
|
| 44 |
+
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
// Prefetch pages for smoother navigation
|
| 47 |
+
router.prefetch('/settings')
|
| 48 |
+
router.prefetch('/about')
|
| 49 |
+
}, [router])
|
| 50 |
+
|
| 51 |
useEffect(() => {
|
| 52 |
+
if (!isTauri()) return
|
| 53 |
+
|
| 54 |
+
void updateMaximizedState()
|
| 55 |
+
|
| 56 |
+
let unlisten: (() => void) | undefined
|
| 57 |
+
const setup = async () => {
|
| 58 |
+
unlisten = await listen('tauri://resize', () => {
|
| 59 |
+
void updateMaximizedState()
|
| 60 |
+
})
|
| 61 |
}
|
| 62 |
+
void setup()
|
| 63 |
|
| 64 |
+
return () => unlisten?.()
|
| 65 |
+
}, [updateMaximizedState])
|
| 66 |
|
| 67 |
const fileMenuItems: MenuItem[] = [
|
| 68 |
{ label: t('menu.openFile'), onSelect: openDocuments },
|
|
|
|
| 86 |
{ label: t('menu.processAll'), onSelect: processAllImages },
|
| 87 |
],
|
| 88 |
},
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
const helpMenuItems: MenuItem[] = [
|
| 92 |
{
|
| 93 |
+
label: t('menu.discord'),
|
| 94 |
+
onSelect: () => openExternal('https://discord.gg/mHvHkxGnUY'),
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
label: t('menu.github'),
|
| 98 |
+
onSelect: () => openExternal('https://github.com/mayocream/koharu'),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
},
|
| 100 |
]
|
| 101 |
|
| 102 |
return (
|
| 103 |
+
<div className='border-border bg-background text-foreground flex h-8 items-center border-b text-[13px]'>
|
| 104 |
+
{/* Logo */}
|
| 105 |
+
<div className='flex h-full items-center pl-2 select-none'>
|
| 106 |
+
<Image
|
| 107 |
+
src='/icon.png'
|
| 108 |
+
alt='Koharu'
|
| 109 |
+
width={18}
|
| 110 |
+
height={18}
|
| 111 |
+
draggable={false}
|
| 112 |
+
/>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
{/* Menu items */}
|
| 116 |
+
<Menubar className='h-auto gap-1 border-none bg-transparent p-0 px-1.5 shadow-none'>
|
| 117 |
<MenubarMenu>
|
| 118 |
<MenubarTrigger className='hover:bg-accent data-[state=open]:bg-accent rounded px-3 py-1.5 font-medium'>
|
| 119 |
{t('menu.file')}
|
|
|
|
| 181 |
))}
|
| 182 |
<MenubarMenu>
|
| 183 |
<MenubarTrigger className='hover:bg-accent data-[state=open]:bg-accent rounded px-3 py-1.5 font-medium'>
|
| 184 |
+
{t('menu.help')}
|
| 185 |
</MenubarTrigger>
|
| 186 |
<MenubarContent
|
| 187 |
className='min-w-36'
|
|
|
|
| 189 |
sideOffset={5}
|
| 190 |
alignOffset={-3}
|
| 191 |
>
|
| 192 |
+
{helpMenuItems.map((item) => (
|
| 193 |
+
<MenubarItem
|
| 194 |
+
key={item.label}
|
| 195 |
+
className='text-[13px]'
|
| 196 |
+
disabled={item.disabled}
|
| 197 |
+
onSelect={
|
| 198 |
+
item.onSelect
|
| 199 |
+
? () => {
|
| 200 |
+
void item.onSelect?.()
|
| 201 |
+
}
|
| 202 |
+
: undefined
|
| 203 |
}
|
| 204 |
+
>
|
| 205 |
+
{item.label}
|
| 206 |
+
</MenubarItem>
|
| 207 |
+
))}
|
| 208 |
+
<MenubarSeparator />
|
| 209 |
+
<MenubarItem
|
| 210 |
+
className='text-[13px]'
|
| 211 |
+
onSelect={() => router.push('/about')}
|
| 212 |
>
|
| 213 |
+
{t('settings.about')}
|
| 214 |
+
</MenubarItem>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
</MenubarContent>
|
| 216 |
</MenubarMenu>
|
| 217 |
</Menubar>
|
| 218 |
+
|
| 219 |
+
{/* Draggable region */}
|
| 220 |
+
<div
|
| 221 |
+
data-tauri-drag-region
|
| 222 |
+
className='flex h-full flex-1 items-center justify-center'
|
| 223 |
+
/>
|
| 224 |
+
|
| 225 |
+
{/* Window controls */}
|
| 226 |
+
{isTauri() && (
|
| 227 |
+
<div className='flex h-full items-center'>
|
| 228 |
+
<button
|
| 229 |
+
type='button'
|
| 230 |
+
onClick={() => void windowControls.minimize()}
|
| 231 |
+
className='hover:bg-accent flex h-full w-12 items-center justify-center transition-colors'
|
| 232 |
+
aria-label='Minimize'
|
| 233 |
+
>
|
| 234 |
+
<Minus className='size-4' />
|
| 235 |
+
</button>
|
| 236 |
+
<button
|
| 237 |
+
type='button'
|
| 238 |
+
onClick={() => void windowControls.toggleMaximize()}
|
| 239 |
+
className='hover:bg-accent flex h-full w-12 items-center justify-center transition-colors'
|
| 240 |
+
aria-label={isMaximized ? 'Restore' : 'Maximize'}
|
| 241 |
+
>
|
| 242 |
+
{isMaximized ? (
|
| 243 |
+
<Copy className='size-3.5' />
|
| 244 |
+
) : (
|
| 245 |
+
<Square className='size-3.5' />
|
| 246 |
+
)}
|
| 247 |
+
</button>
|
| 248 |
+
<button
|
| 249 |
+
type='button'
|
| 250 |
+
onClick={() => void windowControls.close()}
|
| 251 |
+
className='flex h-full w-12 items-center justify-center transition-colors hover:bg-red-500 hover:text-white'
|
| 252 |
+
aria-label='Close'
|
| 253 |
+
>
|
| 254 |
+
<X className='size-4' />
|
| 255 |
+
</button>
|
| 256 |
+
</div>
|
| 257 |
+
)}
|
| 258 |
</div>
|
| 259 |
)
|
| 260 |
}
|
ui/lib/backend.ts
CHANGED
|
@@ -249,3 +249,31 @@ export async function fetchThumbnail(index: number): Promise<Blob> {
|
|
| 249 |
}
|
| 250 |
|
| 251 |
export const isTauri = isTauriEnv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
}
|
| 250 |
|
| 251 |
export const isTauri = isTauriEnv
|
| 252 |
+
|
| 253 |
+
export const windowControls = {
|
| 254 |
+
async minimize() {
|
| 255 |
+
if (isTauriEnv()) {
|
| 256 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 257 |
+
return getCurrentWindow().minimize()
|
| 258 |
+
}
|
| 259 |
+
},
|
| 260 |
+
async toggleMaximize() {
|
| 261 |
+
if (isTauriEnv()) {
|
| 262 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 263 |
+
return getCurrentWindow().toggleMaximize()
|
| 264 |
+
}
|
| 265 |
+
},
|
| 266 |
+
async close() {
|
| 267 |
+
if (isTauriEnv()) {
|
| 268 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 269 |
+
return getCurrentWindow().close()
|
| 270 |
+
}
|
| 271 |
+
},
|
| 272 |
+
async isMaximized(): Promise<boolean> {
|
| 273 |
+
if (isTauriEnv()) {
|
| 274 |
+
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
| 275 |
+
return getCurrentWindow().isMaximized()
|
| 276 |
+
}
|
| 277 |
+
return false
|
| 278 |
+
},
|
| 279 |
+
}
|
ui/public/icon-large.png
ADDED
|
|
Git LFS Details
|
ui/public/icon.png
ADDED
|
|
Git LFS Details
|
ui/public/locales/en-US/translation.json
CHANGED
|
@@ -147,9 +147,22 @@
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "Settings",
|
|
|
|
|
|
|
| 150 |
"theme": "Theme",
|
|
|
|
| 151 |
"themeLight": "Light",
|
| 152 |
"themeDark": "Dark",
|
| 153 |
-
"themeSystem": "System"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
}
|
|
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "Settings",
|
| 150 |
+
"appearance": "Appearance",
|
| 151 |
+
"appearanceDescription": "Customize how Koharu looks on your device.",
|
| 152 |
"theme": "Theme",
|
| 153 |
+
"themeDescription": "Select your preferred color scheme.",
|
| 154 |
"themeLight": "Light",
|
| 155 |
"themeDark": "Dark",
|
| 156 |
+
"themeSystem": "System",
|
| 157 |
+
"language": "Language",
|
| 158 |
+
"languageDescription": "Select your preferred language.",
|
| 159 |
+
"about": "About",
|
| 160 |
+
"aboutDescription": "Information about this application.",
|
| 161 |
+
"aboutTagline": "Manga translation tool",
|
| 162 |
+
"aboutVersion": "Version",
|
| 163 |
+
"aboutLatest": "Latest",
|
| 164 |
+
"aboutUpdate": "Update available ({{version}})",
|
| 165 |
+
"aboutAuthor": "Author",
|
| 166 |
+
"aboutRepository": "Repository"
|
| 167 |
}
|
| 168 |
}
|
ui/public/locales/ja-JP/translation.json
CHANGED
|
@@ -147,9 +147,22 @@
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "設定",
|
|
|
|
|
|
|
| 150 |
"theme": "テーマ",
|
|
|
|
| 151 |
"themeLight": "ライト",
|
| 152 |
"themeDark": "ダーク",
|
| 153 |
-
"themeSystem": "システム"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
}
|
|
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "設定",
|
| 150 |
+
"appearance": "外観",
|
| 151 |
+
"appearanceDescription": "Koharu の外観をカスタマイズします。",
|
| 152 |
"theme": "テーマ",
|
| 153 |
+
"themeDescription": "お好みの配色を選択してください。",
|
| 154 |
"themeLight": "ライト",
|
| 155 |
"themeDark": "ダーク",
|
| 156 |
+
"themeSystem": "システム",
|
| 157 |
+
"language": "言語",
|
| 158 |
+
"languageDescription": "お好みの言語を選択してください。",
|
| 159 |
+
"about": "このアプリについて",
|
| 160 |
+
"aboutDescription": "このアプリケーションに関する情報。",
|
| 161 |
+
"aboutTagline": "漫画翻訳ツール",
|
| 162 |
+
"aboutVersion": "バージョン",
|
| 163 |
+
"aboutLatest": "最新",
|
| 164 |
+
"aboutUpdate": "アップデートあり ({{version}})",
|
| 165 |
+
"aboutAuthor": "作者",
|
| 166 |
+
"aboutRepository": "リポジトリ"
|
| 167 |
}
|
| 168 |
}
|
ui/public/locales/zh-CN/translation.json
CHANGED
|
@@ -147,9 +147,22 @@
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "设置",
|
|
|
|
|
|
|
| 150 |
"theme": "主题",
|
|
|
|
| 151 |
"themeLight": "浅色",
|
| 152 |
"themeDark": "深色",
|
| 153 |
-
"themeSystem": "跟随系统"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
}
|
|
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "设置",
|
| 150 |
+
"appearance": "外观",
|
| 151 |
+
"appearanceDescription": "自定义 Koharu 在您设备上的外观。",
|
| 152 |
"theme": "主题",
|
| 153 |
+
"themeDescription": "选择您偏好的配色方案。",
|
| 154 |
"themeLight": "浅色",
|
| 155 |
"themeDark": "深色",
|
| 156 |
+
"themeSystem": "跟随系统",
|
| 157 |
+
"language": "语言",
|
| 158 |
+
"languageDescription": "选择您偏好的语言。",
|
| 159 |
+
"about": "关于",
|
| 160 |
+
"aboutDescription": "关于此应用程序的信息。",
|
| 161 |
+
"aboutTagline": "漫画翻译工具",
|
| 162 |
+
"aboutVersion": "版本",
|
| 163 |
+
"aboutLatest": "最新",
|
| 164 |
+
"aboutUpdate": "有更新可用 ({{version}})",
|
| 165 |
+
"aboutAuthor": "作者",
|
| 166 |
+
"aboutRepository": "代码仓库"
|
| 167 |
}
|
| 168 |
}
|
ui/public/locales/zh-TW/translation.json
CHANGED
|
@@ -147,9 +147,22 @@
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "設定",
|
|
|
|
|
|
|
| 150 |
"theme": "主題",
|
|
|
|
| 151 |
"themeLight": "淺色",
|
| 152 |
"themeDark": "深色",
|
| 153 |
-
"themeSystem": "跟隨系統"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
}
|
| 155 |
}
|
|
|
|
| 147 |
},
|
| 148 |
"settings": {
|
| 149 |
"title": "設定",
|
| 150 |
+
"appearance": "外觀",
|
| 151 |
+
"appearanceDescription": "自訂 Koharu 在您裝置上的外觀。",
|
| 152 |
"theme": "主題",
|
| 153 |
+
"themeDescription": "選擇您偏好的配色方案。",
|
| 154 |
"themeLight": "淺色",
|
| 155 |
"themeDark": "深色",
|
| 156 |
+
"themeSystem": "跟隨系統",
|
| 157 |
+
"language": "語言",
|
| 158 |
+
"languageDescription": "選擇您偏好的語言。",
|
| 159 |
+
"about": "關於",
|
| 160 |
+
"aboutDescription": "關於此應用程式的資訊。",
|
| 161 |
+
"aboutTagline": "漫畫翻譯工具",
|
| 162 |
+
"aboutVersion": "版本",
|
| 163 |
+
"aboutLatest": "最新",
|
| 164 |
+
"aboutUpdate": "有更新可用 ({{version}})",
|
| 165 |
+
"aboutAuthor": "作者",
|
| 166 |
+
"aboutRepository": "原始碼"
|
| 167 |
}
|
| 168 |
}
|