mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-25 14:53:32 +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() {
|
export function AppShell() {
|
||||||
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
const fetchSettings = useSettingsStore((s) => s.fetchAll)
|
||||||
|
const uiZoom = useSettingsStore((s) => s.uiZoom)
|
||||||
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
|
const sidebarOpen = useUIStore((s) => s.sidebarOpen)
|
||||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar)
|
const toggleSidebar = useUIStore((s) => s.toggleSidebar)
|
||||||
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
|
const setSidebarOpen = useUIStore((s) => s.setSidebarOpen)
|
||||||
@ -194,7 +195,7 @@ export function AppShell() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 ? (
|
{isMobileShell && effectiveSidebarOpen ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -583,7 +583,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isMobile && (
|
{!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
|
<NavItem
|
||||||
active={activeTabId === SETTINGS_TAB_ID}
|
active={activeTabId === SETTINGS_TAB_ID}
|
||||||
collapsed={!expanded}
|
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.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.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.webSearchSave': 'Save',
|
||||||
|
'settings.general.uiZoom': 'UI Zoom',
|
||||||
|
'settings.general.uiZoomDescription': 'Scale the entire interface for better visibility on high-DPI displays.',
|
||||||
|
|
||||||
// ─── Empty Session ──────────────────────────────────────
|
// ─── Empty Session ──────────────────────────────────────
|
||||||
'empty.title': 'New session',
|
'empty.title': 'New session',
|
||||||
|
|||||||
@ -789,6 +789,8 @@ export const zh: Record<TranslationKey, string> = {
|
|||||||
'settings.general.webSearchBraveFreeHint': '注册账号后可创建 Search API Key,免费额度可用于测试。',
|
'settings.general.webSearchBraveFreeHint': '注册账号后可创建 Search API Key,免费额度可用于测试。',
|
||||||
'settings.general.webSearchHint': '自动模式会对 Claude 模型名优先使用原生 WebSearch,失败或非 Claude 模型时再使用 Tavily/Brave。',
|
'settings.general.webSearchHint': '自动模式会对 Claude 模型名优先使用原生 WebSearch,失败或非 Claude 模型时再使用 Tavily/Brave。',
|
||||||
'settings.general.webSearchSave': '保存',
|
'settings.general.webSearchSave': '保存',
|
||||||
|
'settings.general.uiZoom': '界面缩放',
|
||||||
|
'settings.general.uiZoomDescription': '缩放整个界面,以便在高 DPI 屏幕上获得更好的可见性。',
|
||||||
|
|
||||||
// ─── Empty Session ──────────────────────────────────────
|
// ─── Empty Session ──────────────────────────────────────
|
||||||
'empty.title': '新建会话',
|
'empty.title': '新建会话',
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'
|
import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react'
|
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 { useProviderStore } from '../stores/providerStore'
|
||||||
import { useTranslation } from '../i18n'
|
import { useTranslation } from '../i18n'
|
||||||
import { Modal } from '../components/shared/Modal'
|
import { Modal } from '../components/shared/Modal'
|
||||||
@ -1386,6 +1386,8 @@ function GeneralSettings() {
|
|||||||
appModeRequiresRestart,
|
appModeRequiresRestart,
|
||||||
fetchAppMode,
|
fetchAppMode,
|
||||||
setAppMode: setAppModeAction,
|
setAppMode: setAppModeAction,
|
||||||
|
uiZoom,
|
||||||
|
setUiZoom,
|
||||||
} = useSettingsStore()
|
} = useSettingsStore()
|
||||||
const t = useTranslation()
|
const t = useTranslation()
|
||||||
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
|
const [webSearchDraft, setWebSearchDraft] = useState(webSearch)
|
||||||
@ -1859,6 +1861,32 @@ function GeneralSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 */}
|
{/* Confirm dialog for mode switch */}
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|||||||
@ -9,6 +9,10 @@ import type { Locale } from '../i18n'
|
|||||||
import { useUIStore } from './uiStore'
|
import { useUIStore } from './uiStore'
|
||||||
|
|
||||||
const LOCALE_STORAGE_KEY = 'cc-haha-locale'
|
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()
|
let desktopNotificationsSaveQueue: Promise<void> = Promise.resolve()
|
||||||
|
|
||||||
function getStoredLocale(): Locale {
|
function getStoredLocale(): Locale {
|
||||||
@ -19,6 +23,17 @@ function getStoredLocale(): Locale {
|
|||||||
return 'zh'
|
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 = {
|
type SettingsStore = {
|
||||||
permissionMode: PermissionMode
|
permissionMode: PermissionMode
|
||||||
currentModel: ModelInfo | null
|
currentModel: ModelInfo | null
|
||||||
@ -34,6 +49,7 @@ type SettingsStore = {
|
|||||||
h5Access: H5AccessSettings
|
h5Access: H5AccessSettings
|
||||||
h5AccessError: string | null
|
h5AccessError: string | null
|
||||||
responseLanguage: string
|
responseLanguage: string
|
||||||
|
uiZoom: number
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
|
|
||||||
@ -61,6 +77,7 @@ type SettingsStore = {
|
|||||||
setResponseLanguage: (language: string) => Promise<void>
|
setResponseLanguage: (language: string) => Promise<void>
|
||||||
fetchAppMode: () => Promise<void>
|
fetchAppMode: () => Promise<void>
|
||||||
setAppMode: (mode: AppMode, portableDir?: string | null) => Promise<void>
|
setAppMode: (mode: AppMode, portableDir?: string | null) => Promise<void>
|
||||||
|
setUiZoom: (zoom: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
|
const DEFAULT_H5_ACCESS_SETTINGS: H5AccessSettings = {
|
||||||
@ -85,11 +102,17 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|||||||
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
|
h5Access: DEFAULT_H5_ACCESS_SETTINGS,
|
||||||
h5AccessError: null,
|
h5AccessError: null,
|
||||||
responseLanguage: '',
|
responseLanguage: '',
|
||||||
|
uiZoom: getStoredUiZoom(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|
||||||
appMode: { mode: 'default', portableDir: null, defaultPortableDir: null },
|
appMode: { mode: 'default', portableDir: null, defaultPortableDir: null },
|
||||||
appModeRequiresRestart: false,
|
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 () => {
|
fetchAll: async () => {
|
||||||
set({ isLoading: true, error: null })
|
set({ isLoading: true, error: null })
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export type UserSettings = {
|
|||||||
desktopNotificationsEnabled?: boolean
|
desktopNotificationsEnabled?: boolean
|
||||||
webSearch?: WebSearchSettings
|
webSearch?: WebSearchSettings
|
||||||
language?: string
|
language?: string
|
||||||
|
uiZoom?: number
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user