mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Merge branch 'main' into main
This commit is contained in:
commit
4edbee9f52
@ -28,6 +28,7 @@ function isChatTab(tab: Tab | undefined) {
|
||||
|
||||
export function AppShell() {
|
||||
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
||||
const uiZoom = useSettingsStore((s) => s.uiZoom)
|
||||
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar)
|
||||
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
|
||||
@ -194,7 +195,7 @@ export function AppShell() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`app-shell app-shell-viewport flex overflow-hidden bg-[var(--color-surface)]${isMobileShell ? ' app-shell--mobile' : ''}`}>
|
||||
<div className={`app-shell app-shell-viewport flex overflow-hidden bg-[var(--color-surface)]${isMobileShell ? ' app-shell--mobile' : ''}`} style={{ zoom: uiZoom }}>
|
||||
{isMobileShell && effectiveSidebarOpen ? (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -583,7 +583,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
|
||||
)}
|
||||
|
||||
{!isMobile && (
|
||||
<div className={`border-t border-[var(--color-border)] p-3 ${expanded ? '' : 'flex justify-center'}`}>
|
||||
<div className={`absolute bottom-0 left-0 right-0 border-t border-[var(--color-border)] p-3 ${expanded ? '' : 'flex justify-center'}`}>
|
||||
<NavItem
|
||||
active={activeTabId === SETTINGS_TAB_ID}
|
||||
collapsed={!expanded}
|
||||
|
||||
@ -786,6 +786,8 @@ export const en = {
|
||||
'settings.general.webSearchBraveFreeHint': 'Create an account to generate a Search API key with free usage for testing.',
|
||||
'settings.general.webSearchHint': 'Auto uses native Claude web search for Claude model names, then falls back to Tavily and Brave keys.',
|
||||
'settings.general.webSearchSave': 'Save',
|
||||
'settings.general.uiZoom': 'UI Zoom',
|
||||
'settings.general.uiZoomDescription': 'Scale the entire interface for better visibility on high-DPI displays.',
|
||||
|
||||
// ─── Empty Session ──────────────────────────────────────
|
||||
'empty.title': 'New session',
|
||||
|
||||
@ -789,6 +789,8 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.webSearchBraveFreeHint': '注册账号后可创建 Search API Key,免费额度可用于测试。',
|
||||
'settings.general.webSearchHint': '自动模式会对 Claude 模型名优先使用原生 WebSearch,失败或非 Claude 模型时再使用 Tavily/Brave。',
|
||||
'settings.general.webSearchSave': '保存',
|
||||
'settings.general.uiZoom': '界面缩放',
|
||||
'settings.general.uiZoomDescription': '缩放整个界面,以便在高 DPI 屏幕上获得更好的可见性。',
|
||||
|
||||
// ─── Empty Session ──────────────────────────────────────
|
||||
'empty.title': '新建会话',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'
|
||||
import QRCode from 'qrcode'
|
||||
import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react'
|
||||
import { useSettingsStore } from '../stores/settingsStore'
|
||||
import { useSettingsStore, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore'
|
||||
import { useProviderStore } from '../stores/providerStore'
|
||||
import { useTranslation } from '../i18n'
|
||||
import { Modal } from '../components/shared/Modal'
|
||||
@ -1386,6 +1386,8 @@ function GeneralSettings() {
|
||||
appModeRequiresRestart,
|
||||
fetchAppMode,
|
||||
setAppMode: setAppModeAction,
|
||||
uiZoom,
|
||||
setUiZoom,
|
||||
} = useSettingsStore()
|
||||
const t = useTranslation()
|
||||
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
|
||||
@ -1859,6 +1861,32 @@ function GeneralSettings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* UI Zoom */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-base font-semibold text-[var(--color-text-primary)] mb-1">{t('settings.general.uiZoom')}</h2>
|
||||
<p className="text-sm text-[var(--color-text-tertiary)] mb-3">{t('settings.general.uiZoomDescription')}</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-end">
|
||||
<span className="text-sm text-[var(--color-text-secondary)]">
|
||||
{Math.round(uiZoom * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MIN * 100)}%</span>
|
||||
<input
|
||||
type="range"
|
||||
min={UI_ZOOM_MIN}
|
||||
max={UI_ZOOM_MAX}
|
||||
step={UI_ZOOM_STEP}
|
||||
value={uiZoom}
|
||||
onChange={(e) => setUiZoom(parseFloat(e.target.value))}
|
||||
onMouseUp={(e) => e.currentTarget.blur()}
|
||||
className="flex-1"
|
||||
/>
|
||||
<span className="text-xs text-[var(--color-text-tertiary)]">{Math.round(UI_ZOOM_MAX * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confirm dialog for mode switch */}
|
||||
<ConfirmDialog
|
||||
|
||||
@ -9,6 +9,10 @@ import type { Locale } from '../i18n'
|
||||
import { useUIStore } from './uiStore'
|
||||
|
||||
const LOCALE_STORAGE_KEY = 'cc-haha-locale'
|
||||
const UI_ZOOM_STORAGE_KEY = 'cc-haha-ui-zoom'
|
||||
export const UI_ZOOM_MIN = 0.5
|
||||
export const UI_ZOOM_MAX = 2.0
|
||||
export const UI_ZOOM_STEP = 0.05
|
||||
let desktopNotificationsSaveQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
function getStoredLocale(): Locale {
|
||||
@ -19,6 +23,17 @@ function getStoredLocale(): Locale {
|
||||
return 'zh'
|
||||
}
|
||||
|
||||
function getStoredUiZoom(): number {
|
||||
try {
|
||||
const stored = localStorage.getItem(UI_ZOOM_STORAGE_KEY)
|
||||
if (stored === null) return 1.0
|
||||
const parsed = parseFloat(stored)
|
||||
if (isNaN(parsed)) return 1.0
|
||||
return Math.min(UI_ZOOM_MAX, Math.max(UI_ZOOM_MIN, parsed))
|
||||
} catch { /* localStorage unavailable */ }
|
||||
return 1.0
|
||||
}
|
||||
|
||||
type SettingsStore = {
|
||||
permissionMode: PermissionMode
|
||||
currentModel: ModelInfo | null
|
||||
@ -34,6 +49,7 @@ type SettingsStore = {
|
||||
h5Access: H5AccessSettings
|
||||
h5AccessError: string | null
|
||||
responseLanguage: string
|
||||
uiZoom: number
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
|
||||
@ -61,6 +77,7 @@ type SettingsStore = {
|
||||
setResponseLanguage: (language: string) => Promise<void>
|
||||
fetchAppMode: () => Promise<void>
|
||||
setAppMode: (mode: AppMode, portableDir?: string | null) => Promise<void>
|
||||
setUiZoom: (zoom: number) => void
|
||||
}
|
||||
|
||||
const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
|
||||
@ -85,11 +102,17 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
||||
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
|
||||
h5AccessError: null,
|
||||
responseLanguage: '',
|
||||
uiZoom: getStoredUiZoom(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
appMode: { mode: 'default', portableDir: null, defaultPortableDir: null },
|
||||
appModeRequiresRestart: false,
|
||||
setUiZoom: (zoom: number) => {
|
||||
const clamped = Math.min(UI_ZOOM_MAX, Math.max(UI_ZOOM_MIN, zoom))
|
||||
set({ uiZoom: clamped })
|
||||
try { localStorage.setItem(UI_ZOOM_STORAGE_KEY, String(clamped)) } catch { /* noop */ }
|
||||
},
|
||||
|
||||
fetchAll: async () => {
|
||||
set({ isLoading: true, error: null })
|
||||
|
||||
@ -43,6 +43,7 @@ export type UserSettings = {
|
||||
desktopNotificationsEnabled?: boolean
|
||||
webSearch?: WebSearchSettings
|
||||
language?: string
|
||||
uiZoom?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user