From 076eb7b9dee07453106b35790162eb3c5d7ea02e Mon Sep 17 00:00:00 2001 From: Zhiqiang ZHOU Date: Wed, 31 Dec 2025 18:07:06 -0800 Subject: [PATCH 1/2] feat: implement automatic language preference persistence per channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a user selects a language for a video from a specific channel, the app now remembers and automatically applies that preference when processing future videos from the same channel. Preferences are stored per platform and channel identifier (with fallback to channel name), persisted to settings.json, and gracefully handle backward compatibility with existing settings files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- app.go | 64 ++++++++++++++++--- frontend/package-lock.json | 70 +++++---------------- frontend/src/pages/NewTranscriptionPage.tsx | 24 ++++++- frontend/src/pages/SettingsPage.tsx | 3 +- frontend/wailsjs/go/main/App.d.ts | 4 ++ frontend/wailsjs/go/main/App.js | 8 +++ frontend/wailsjs/go/models.ts | 2 + internal/types/types.go | 17 ++--- 8 files changed, 116 insertions(+), 76 deletions(-) diff --git a/app.go b/app.go index 3a1a1d1..df147ce 100644 --- a/app.go +++ b/app.go @@ -62,14 +62,15 @@ func NewApp() *App { logger: logger, summarizer: services.NewOpenRouterClient(), settings: types.Settings{ - Workspace: storage.GetWorkspace(), - SourceLang: "en", - APIProvider: "openrouter", - APIKey: "", - SummaryLength: "medium", - SummaryLanguage: "en", - Temperature: 0.3, - MaxTokens: 4096, + Workspace: storage.GetWorkspace(), + SourceLang: "en", + APIProvider: "openrouter", + APIKey: "", + SummaryLength: "medium", + SummaryLanguage: "en", + Temperature: 0.3, + MaxTokens: 4096, + ChannelLanguagePrefs: make(map[string]string), }, settingsStore: ss, } @@ -103,6 +104,10 @@ func (a *App) startup(ctx context.Context) { a.logger.Warn("Failed to load settings", "error", err) } else if loaded != nil { a.settings = *loaded + // Initialize ChannelLanguagePrefs if nil (for backward compatibility) + if a.settings.ChannelLanguagePrefs == nil { + a.settings.ChannelLanguagePrefs = make(map[string]string) + } // keep storage workspace in sync if a.settings.Workspace != "" { a.storage.SetWorkspace(a.settings.Workspace) @@ -154,6 +159,44 @@ func (a *App) UpdateSettings(settings types.Settings) types.Settings { return a.settings } +// buildChannelKey creates a unique key for channel language preferences +func buildChannelKey(platform, channelID, channelName string) string { + identifier := channelID + if identifier == "" { + identifier = channelName + } + return fmt.Sprintf("%s:%s", platform, identifier) +} + +// GetChannelLanguagePreference returns the saved language preference for a channel +func (a *App) GetChannelLanguagePreference(platform, channelID, channelName string) string { + if a.settings.ChannelLanguagePrefs == nil { + return a.settings.SourceLang + } + + key := buildChannelKey(platform, channelID, channelName) + if lang, ok := a.settings.ChannelLanguagePrefs[key]; ok { + return lang + } + + return a.settings.SourceLang +} + +// SetChannelLanguagePreference saves the language preference for a channel +func (a *App) SetChannelLanguagePreference(platform, channelID, channelName, lang string) error { + if a.settings.ChannelLanguagePrefs == nil { + a.settings.ChannelLanguagePrefs = make(map[string]string) + } + + key := buildChannelKey(platform, channelID, channelName) + a.settings.ChannelLanguagePrefs[key] = lang + + if a.settingsStore != nil { + return a.settingsStore.Save(a.settings) + } + return nil +} + // DetectPlatform detects which video platform the URL belongs to func (a *App) DetectPlatform(url string) string { return a.downloader.DetectPlatform(url) @@ -228,6 +271,11 @@ func (a *App) StartTranscription(url string, sourceLang string) (*types.Task, er platform := a.downloader.DetectPlatform(url) + // Save channel language preference + if err := a.SetChannelLanguagePreference(platform, info.ChannelID, info.Channel, sourceLang); err != nil { + a.logger.Warn("Failed to save channel language preference", "error", err) + } + task, err := a.taskManager.CreateTask( url, sourceLang, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 685d8ef..c3c5583 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -97,6 +97,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -419,7 +420,6 @@ "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -508,7 +508,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -531,7 +530,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -554,7 +552,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -571,7 +568,6 @@ "os": [ "darwin" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -588,7 +584,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -605,7 +600,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -622,7 +616,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -639,7 +632,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -656,7 +648,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -673,7 +664,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -690,7 +680,6 @@ "os": [ "linux" ], - "peer": true, "funding": { "url": "https://opencollective.com/libvips" } @@ -707,7 +696,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -730,7 +718,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -753,7 +740,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -776,7 +762,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -799,7 +784,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -822,7 +806,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -845,7 +828,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -865,7 +847,6 @@ ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, - "peer": true, "dependencies": { "@emnapi/runtime": "^1.4.4" }, @@ -888,7 +869,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -908,7 +888,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -928,7 +907,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -982,7 +960,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1010,8 +987,7 @@ "version": "15.5.2", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { "version": "15.5.2", @@ -1025,7 +1001,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1042,7 +1017,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1059,7 +1033,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1076,7 +1049,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1093,7 +1065,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1110,7 +1081,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1127,7 +1097,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1144,7 +1113,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 10" } @@ -1965,7 +1933,6 @@ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.8.0" } @@ -1983,6 +1950,7 @@ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1994,6 +1962,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2187,6 +2156,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -2206,8 +2176,7 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/camelcase-css": { "version": "2.0.1", @@ -2293,8 +2262,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/clsx": { "version": "2.1.1", @@ -2311,7 +2279,6 @@ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" @@ -2346,7 +2313,6 @@ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -2437,7 +2403,6 @@ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", "license": "Apache-2.0", "optional": true, - "peer": true, "engines": { "node": ">=8" } @@ -3051,8 +3016,7 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/is-binary-path": { "version": "2.1.0", @@ -3347,7 +3311,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", @@ -3425,7 +3388,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -3589,6 +3551,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3745,6 +3708,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3757,6 +3721,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4009,7 +3974,6 @@ "hasInstallScript": true, "license": "Apache-2.0", "optional": true, - "peer": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", @@ -4052,7 +4016,6 @@ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "optional": true, - "peer": true, "bin": { "semver": "bin/semver.js" }, @@ -4102,7 +4065,6 @@ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "is-arrayish": "^0.3.1" } @@ -4114,7 +4076,6 @@ "dev": true, "license": "BSD-3-Clause", "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -4135,7 +4096,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -4258,7 +4218,6 @@ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", - "peer": true, "dependencies": { "client-only": "0.0.1" }, @@ -4329,6 +4288,7 @@ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4378,7 +4338,6 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", @@ -4399,7 +4358,6 @@ "dev": true, "license": "MIT", "optional": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4413,8 +4371,7 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/thenify": { "version": "3.3.1", @@ -4566,6 +4523,7 @@ "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.15.9", "postcss": "^8.4.18", diff --git a/frontend/src/pages/NewTranscriptionPage.tsx b/frontend/src/pages/NewTranscriptionPage.tsx index a6f81cf..d860cf9 100644 --- a/frontend/src/pages/NewTranscriptionPage.tsx +++ b/frontend/src/pages/NewTranscriptionPage.tsx @@ -7,7 +7,7 @@ import { AlertCircle, ChevronLeft, Download } from 'lucide-react' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import VideoPreview, { VideoMetadata } from '@/components/VideoPreview' import TaskProgress, { TaskStage } from '@/components/TaskProgress' -import { CheckDependencies, StartTranscription, GetTask, DetectPlatform } from '../../wailsjs/go/main/App' +import { CheckDependencies, StartTranscription, GetTask, DetectPlatform, GetChannelLanguagePreference } from '../../wailsjs/go/main/App' import { useDebounce } from '@/hooks' export default function NewTranscriptionPage() { @@ -182,6 +182,24 @@ export default function NewTranscriptionPage() { startProcessing() } + const handleMetadataLoaded = async (metadata: VideoMetadata) => { + setVideoMetadata(metadata) + if (metadata.isValid && metadata.channel) { + try { + const preferredLang = await GetChannelLanguagePreference( + platform, + metadata.channelId || '', + metadata.channel + ) + if (preferredLang) { + setSourceLang(preferredLang) + } + } catch (err) { + console.error('Failed to get channel language preference:', err) + } + } + } + return (
{/* Header */} @@ -219,9 +237,9 @@ export default function NewTranscriptionPage() { {/* Video Preview */} {url && ( - )} diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 5f576c1..4a8ba29 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -23,7 +23,8 @@ export default function SettingsPage() { summaryLength: 'medium', summaryLanguage: 'en', temperature: 0.3, - maxTokens: 4096 + maxTokens: 4096, + channelLanguagePrefs: {} }) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 04ffef6..4338f31 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -13,6 +13,8 @@ export function DownloadTask(arg1:string):Promise; export function GetAllTasks():Promise>; +export function GetChannelLanguagePreference(arg1:string,arg2:string,arg3:string):Promise; + export function GetDebugInfo():Promise>; export function GetSettings():Promise; @@ -27,6 +29,8 @@ export function ParseVideoUrl(arg1:string):Promise; export function RetryTask(arg1:string):Promise; +export function SetChannelLanguagePreference(arg1:string,arg2:string,arg3:string,arg4:string):Promise; + export function StartTranscription(arg1:string,arg2:string):Promise; export function SummarizeTask(arg1:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 0cef1cb..9f376cb 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -22,6 +22,10 @@ export function GetAllTasks() { return window['go']['main']['App']['GetAllTasks'](); } +export function GetChannelLanguagePreference(arg1, arg2, arg3) { + return window['go']['main']['App']['GetChannelLanguagePreference'](arg1, arg2, arg3); +} + export function GetDebugInfo() { return window['go']['main']['App']['GetDebugInfo'](); } @@ -50,6 +54,10 @@ export function RetryTask(arg1) { return window['go']['main']['App']['RetryTask'](arg1); } +export function SetChannelLanguagePreference(arg1, arg2, arg3, arg4) { + return window['go']['main']['App']['SetChannelLanguagePreference'](arg1, arg2, arg3, arg4); +} + export function StartTranscription(arg1, arg2) { return window['go']['main']['App']['StartTranscription'](arg1, arg2); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index e45c874..95b282d 100755 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -52,6 +52,7 @@ export namespace types { summaryLanguage: string; temperature: number; maxTokens: number; + channelLanguagePrefs: Record; static createFrom(source: any = {}) { return new Settings(source); @@ -67,6 +68,7 @@ export namespace types { this.summaryLanguage = source["summaryLanguage"]; this.temperature = source["temperature"]; this.maxTokens = source["maxTokens"]; + this.channelLanguagePrefs = source["channelLanguagePrefs"]; } } export class Task { diff --git a/internal/types/types.go b/internal/types/types.go index f31064b..0520e14 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -91,12 +91,13 @@ type DependencyStatus struct { // Settings represents user configuration type Settings struct { - Workspace string `json:"workspace"` - SourceLang string `json:"sourceLang"` - APIProvider string `json:"apiProvider"` // "gemini", "openai", "openrouter" - APIKey string `json:"apiKey"` - SummaryLength string `json:"summaryLength"` // "short", "medium", "long" - SummaryLanguage string `json:"summaryLanguage"` // Language for summaries (e.g., "en", "zh", "ja") - Temperature float64 `json:"temperature"` - MaxTokens int `json:"maxTokens"` + Workspace string `json:"workspace"` + SourceLang string `json:"sourceLang"` + APIProvider string `json:"apiProvider"` + APIKey string `json:"apiKey"` + SummaryLength string `json:"summaryLength"` + SummaryLanguage string `json:"summaryLanguage"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"maxTokens"` + ChannelLanguagePrefs map[string]string `json:"channelLanguagePrefs"` } From 9dd98caa9d20264ff406953549190725a9a63b95 Mon Sep 17 00:00:00 2001 From: Zhiqiang ZHOU Date: Thu, 1 Jan 2026 13:48:08 -0800 Subject: [PATCH 2/2] fix: prevent infinite re-render loop with useCallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handleMetadataLoaded callback was causing VideoPreview's useEffect to re-run on every render because it was creating a new function reference each time. Wrapped with useCallback and proper dependencies to stabilize the reference. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Haiku 4.5 --- frontend/src/pages/NewTranscriptionPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/NewTranscriptionPage.tsx b/frontend/src/pages/NewTranscriptionPage.tsx index d860cf9..5418a2a 100644 --- a/frontend/src/pages/NewTranscriptionPage.tsx +++ b/frontend/src/pages/NewTranscriptionPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import { useNavigate } from 'react-router-dom' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -182,7 +182,7 @@ export default function NewTranscriptionPage() { startProcessing() } - const handleMetadataLoaded = async (metadata: VideoMetadata) => { + const handleMetadataLoaded = useCallback(async (metadata: VideoMetadata) => { setVideoMetadata(metadata) if (metadata.isValid && metadata.channel) { try { @@ -198,7 +198,7 @@ export default function NewTranscriptionPage() { console.error('Failed to get channel language preference:', err) } } - } + }, [platform]) return (