Mayo commited on
Commit
103645b
·
unverified ·
1 Parent(s): 7033f96

feat: about page

Browse files
koharu/capabilities/default.json CHANGED
@@ -8,6 +8,13 @@
8
  ],
9
  "permissions": [
10
  "core:default",
11
- "core:window:allow-set-progress-bar"
 
 
 
 
 
 
 
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
- <main className='bg-background relative flex h-screen w-screen flex-col overflow-hidden'>
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
- </main>
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, useMemo } from 'react'
4
  import { useRouter } from 'next/navigation'
5
- import { invoke } from '@/lib/backend'
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, i18n } = useTranslation()
28
  const router = useRouter()
29
- const locales = useMemo(
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
- const loadVersion = async () => {
45
- try {
46
- const version = await invoke<string>('app_version')
47
- setAppVersion(version)
48
- } catch (error) {
49
- console.error('Failed to load app version', error)
50
- setAppVersion(undefined)
51
- }
 
52
  }
 
53
 
54
- void loadVersion()
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.help'),
81
- items: [
82
- {
83
- label: appVersion
84
- ? t('menu.version', { version: appVersion })
85
- : t('menu.versionUnknown'),
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 gap-1 border-b px-1.5 text-[13px]'>
102
- <Menubar className='h-auto gap-1 border-none bg-transparent p-0 shadow-none'>
 
 
 
 
 
 
 
 
 
 
 
 
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.language')}
171
  </MenubarTrigger>
172
  <MenubarContent
173
  className='min-w-36'
@@ -175,27 +189,72 @@ export function MenuBar() {
175
  sideOffset={5}
176
  alignOffset={-3}
177
  >
178
- <MenubarRadioGroup
179
- value={i18n.language}
180
- onValueChange={(value) => {
181
- if (value !== i18n.language) {
182
- void i18n.changeLanguage(value)
 
 
 
 
 
 
183
  }
184
- }}
 
 
 
 
 
 
 
185
  >
186
- {locales.map((code) => (
187
- <MenubarRadioItem
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

  • SHA256: e1fec0c19e93870cbf481ae14db54b0d95eda9d42b4d03ea1df3bba189eb7a3b
  • Pointer size: 130 Bytes
  • Size of remote file: 60.1 kB
ui/public/icon.png ADDED

Git LFS Details

  • SHA256: 31a76300030d9ae3e855180165966f76f710a9282cbb38796b5869881712eef0
  • Pointer size: 130 Bytes
  • Size of remote file: 22.2 kB
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
  }