feat(settings): improve cookie profile handling (#74)

* feat(settings): improve cookie profile handling

* fix(settings): normalize profile input
This commit is contained in:
Nexmoe
2026-01-11 10:16:28 +08:00
committed by GitHub
parent d2806164a2
commit 4971a5fe8a
5 changed files with 553 additions and 138 deletions

View File

@@ -1,5 +1,6 @@
import { createServices, type MergeIpcService } from 'electron-ipc-decorator'
import { AppService } from './services/app-service'
import { BrowserCookiesService } from './services/browser-cookies-service'
import { DownloadService } from './services/download-service'
import { FileSystemService } from './services/file-system-service'
import { HistoryService } from './services/history-service'
@@ -12,6 +13,7 @@ import { WindowService } from './services/window-service'
// Create services with automatic type inference
export const services = createServices([
AppService,
BrowserCookiesService,
DownloadService,
FileSystemService,
HistoryService,

View File

@@ -0,0 +1,270 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
import { resolvePathWithHome } from '../../utils/path-helpers'
class BrowserCookiesService extends IpcService {
static readonly groupName = 'browserCookies'
private buildValidationResult(valid: boolean, reason?: string) {
if (valid) {
return { valid }
}
return { valid, reason }
}
private isDirectory(target: string): boolean {
try {
return fs.statSync(target).isDirectory()
} catch {
return false
}
}
private pickFirstDirectory(paths: string[]): string {
for (const candidate of paths) {
if (this.isDirectory(candidate)) {
return candidate
}
}
return ''
}
private normalizeProfileInput(value: string): string {
return value.trim().replace(/^['"]|['"]$/g, '')
}
private getBrowserProfileBaseDirs(platform: string, homeDir: string, browser: string): string[] {
if (platform === 'win32') {
if (browser === 'edge') {
return [path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'Edge', 'User Data')]
}
if (browser === 'chrome') {
return [path.join(homeDir, 'AppData', 'Local', 'Google', 'Chrome', 'User Data')]
}
if (browser === 'chromium') {
return [path.join(homeDir, 'AppData', 'Local', 'Chromium', 'User Data')]
}
if (browser === 'brave') {
return [
path.join(homeDir, 'AppData', 'Local', 'BraveSoftware', 'Brave-Browser', 'User Data')
]
}
if (browser === 'vivaldi') {
return [path.join(homeDir, 'AppData', 'Local', 'Vivaldi', 'User Data')]
}
if (browser === 'whale') {
return [path.join(homeDir, 'AppData', 'Local', 'Naver', 'Whale', 'User Data')]
}
if (browser === 'opera') {
return [path.join(homeDir, 'AppData', 'Roaming', 'Opera Software', 'Opera Stable')]
}
if (browser === 'firefox') {
return [path.join(homeDir, 'AppData', 'Roaming', 'Mozilla', 'Firefox', 'Profiles')]
}
}
if (platform === 'darwin') {
if (browser === 'edge') {
return [path.join(homeDir, 'Library', 'Application Support', 'Microsoft Edge')]
}
if (browser === 'chrome') {
return [path.join(homeDir, 'Library', 'Application Support', 'Google', 'Chrome')]
}
if (browser === 'chromium') {
return [path.join(homeDir, 'Library', 'Application Support', 'Chromium')]
}
if (browser === 'brave') {
return [
path.join(homeDir, 'Library', 'Application Support', 'BraveSoftware', 'Brave-Browser')
]
}
if (browser === 'vivaldi') {
return [path.join(homeDir, 'Library', 'Application Support', 'Vivaldi')]
}
if (browser === 'whale') {
return [
path.join(homeDir, 'Library', 'Application Support', 'Whale'),
path.join(homeDir, 'Library', 'Application Support', 'Naver Whale')
]
}
if (browser === 'opera') {
return [
path.join(homeDir, 'Library', 'Application Support', 'com.operasoftware.Opera'),
path.join(homeDir, 'Library', 'Application Support', 'Opera Software', 'Opera Stable')
]
}
if (browser === 'firefox') {
return [path.join(homeDir, 'Library', 'Application Support', 'Firefox', 'Profiles')]
}
if (browser === 'safari') {
return [path.join(homeDir, 'Library', 'Safari')]
}
}
if (platform === 'linux') {
if (browser === 'edge') {
return [path.join(homeDir, '.config', 'microsoft-edge')]
}
if (browser === 'chrome') {
return [path.join(homeDir, '.config', 'google-chrome')]
}
if (browser === 'chromium') {
return [path.join(homeDir, '.config', 'chromium')]
}
if (browser === 'brave') {
return [path.join(homeDir, '.config', 'BraveSoftware', 'Brave-Browser')]
}
if (browser === 'vivaldi') {
return [path.join(homeDir, '.config', 'vivaldi')]
}
if (browser === 'whale') {
return [path.join(homeDir, '.config', 'naver-whale')]
}
if (browser === 'opera') {
return [path.join(homeDir, '.config', 'opera')]
}
if (browser === 'firefox') {
return [path.join(homeDir, '.mozilla', 'firefox')]
}
}
if (platform === 'freebsd') {
if (browser === 'firefox') {
return [path.join(homeDir, '.mozilla', 'firefox')]
}
}
return []
}
private getDefaultProfilePath(baseDirs: string[], browser: string): string {
const base = baseDirs[0]
if (!base) {
return ''
}
if (browser === 'firefox' || browser === 'safari' || browser === 'opera') {
return base
}
return path.join(base, 'Default')
}
private findFirefoxProfilePath(profilesDir: string): string {
if (!this.isDirectory(profilesDir)) {
return ''
}
const entries = fs
.readdirSync(profilesDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort((a, b) => a.localeCompare(b))
const preferred =
entries.find((name) => name.endsWith('.default-release')) ??
entries.find((name) => name.endsWith('.default')) ??
entries[0]
return preferred ? path.join(profilesDir, preferred) : ''
}
@IpcMethod()
getBrowserProfilePath(_context: IpcContext, browser: string): string {
if (!browser || browser === 'none') {
return ''
}
const homeDir = os.homedir()
const platform = os.platform()
const baseDirs = this.getBrowserProfileBaseDirs(platform, homeDir, browser)
const fallbackPath = this.getDefaultProfilePath(baseDirs, browser)
if (browser === 'firefox') {
const profilesDir = baseDirs[0]
const profilePath = profilesDir ? this.findFirefoxProfilePath(profilesDir) : ''
return profilePath || fallbackPath
}
if (browser === 'safari') {
const safariPath = baseDirs[0]
if (safariPath && this.isDirectory(safariPath)) {
return safariPath
}
return fallbackPath
}
if (baseDirs.length === 0) {
return fallbackPath
}
let detectedPath = ''
for (const baseDir of baseDirs) {
if (!baseDir) {
continue
}
const candidates =
browser === 'opera'
? [baseDir, path.join(baseDir, 'Default'), path.join(baseDir, 'Profile 1')]
: [path.join(baseDir, 'Default'), path.join(baseDir, 'Profile 1')]
detectedPath = this.pickFirstDirectory(candidates)
if (detectedPath) {
break
}
}
return detectedPath || fallbackPath
}
@IpcMethod()
validateBrowserProfilePath(
_context: IpcContext,
browser: string,
profilePath: string
): { valid: boolean; reason?: string } {
if (!browser || browser === 'none') {
return this.buildValidationResult(false, 'browserUnsupported')
}
const normalizedInput = this.normalizeProfileInput(profilePath)
if (!normalizedInput) {
return this.buildValidationResult(false, 'empty')
}
const resolvedInput = resolvePathWithHome(normalizedInput)
if (resolvedInput && this.isDirectory(resolvedInput)) {
return this.buildValidationResult(true)
}
const looksLikePath =
resolvedInput &&
(path.isAbsolute(resolvedInput) ||
resolvedInput.includes('/') ||
resolvedInput.includes('\\'))
if (looksLikePath) {
return this.buildValidationResult(false, 'pathNotFound')
}
const platform = os.platform()
const homeDir = os.homedir()
const baseDirs = this.getBrowserProfileBaseDirs(platform, homeDir, browser)
if (baseDirs.length === 0) {
return this.buildValidationResult(false, 'browserUnsupported')
}
for (const baseDir of baseDirs) {
if (!baseDir) {
continue
}
const candidate = path.join(baseDir, normalizedInput)
if (this.isDirectory(candidate)) {
return this.buildValidationResult(true)
}
}
return this.buildValidationResult(false, 'profileNotFound')
}
}
export { BrowserCookiesService }

View File

@@ -253,17 +253,17 @@ export function FormatSelector({
onVideoFormatChange?.(value)
}}
>
<SelectTrigger className="h-11">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px] p-1.5">
<SelectContent>
{videoFormats.map((format) => (
<SelectItem
key={format.format_id}
value={format.format_id}
className="cursor-pointer py-2.5"
className="cursor-pointer"
>
<span className="text-sm">{formatVideoLabel(format)}</span>
<span>{formatVideoLabel(format)}</span>
</SelectItem>
))}
</SelectContent>
@@ -281,18 +281,18 @@ export function FormatSelector({
onAudioFormatChange?.(value)
}}
>
<SelectTrigger className="h-11">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px] p-1.5">
<SelectItem value="none" className="cursor-pointer py-2.5">
<span className="text-sm">{t('download.noAudio')}</span>
<SelectContent>
<SelectItem value="none" className="cursor-pointer">
<span>{t('download.noAudio')}</span>
</SelectItem>
{audioFormats.map((format) => (
<SelectItem
key={format.format_id}
value={format.format_id}
className="cursor-pointer py-2.5"
className="cursor-pointer"
>
<span className="text-sm">{formatAudioLabel(format)}</span>
</SelectItem>
@@ -320,17 +320,13 @@ export function FormatSelector({
onAudioFormatChange?.(value)
}}
>
<SelectTrigger className="h-11">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent className="max-h-[300px] p-1.5">
<SelectContent>
{audioFormats.map((format) => (
<SelectItem
key={format.format_id}
value={format.format_id}
className="cursor-pointer py-2.5"
>
<span className="text-sm">{formatAudioLabel(format)}</span>
<SelectItem key={format.format_id} value={format.format_id} className="cursor-pointer">
<span>{formatAudioLabel(format)}</span>
</SelectItem>
))}
</SelectContent>

View File

@@ -372,7 +372,15 @@
"app": "App Settings",
"audio": "Audio Preferences",
"browserForCookies": "Select browser to use cookies from",
"browserForCookiesDescription": "Browser to extract cookies from for authentication",
"browserForCookiesDescription": "Browser to extract cookies from for authentication. We'll try to detect a profile automatically.",
"browserForCookiesProfile": "Profile name or path",
"browserForCookiesProfileDescription": "Profile path for the browser selected above. Auto-filled when possible.",
"browserForCookiesProfilePlaceholder": "Profile name or full path (optional)",
"browserForCookiesProfileInvalid": "Profile path is not valid. Choose the profile folder for the selected browser.",
"browserForCookiesProfileInvalidPath": "That folder does not exist. Pick an existing profile folder.",
"browserForCookiesProfileInvalidProfile": "Profile name not found in the default browser location.",
"browserForCookiesProfileInvalidUnsupported": "No default profile location is known for this browser on this platform.",
"browserForCookiesProfileInvalidEmpty": "Enter a profile path for the selected browser.",
"cookiesFile": "Cookies file",
"cookiesFileDescription": "Netscape formatted cookies file to load for authentication",
"clearCookiesFile": "Clear",
@@ -387,7 +395,10 @@
"chromium": "Chromium",
"edge": "Edge",
"firefox": "Firefox",
"safari": "Safari"
"opera": "Opera",
"safari": "Safari",
"vivaldi": "Vivaldi",
"whale": "Whale"
},
"configFile": "Use configuration file",
"configFileDescription": "Custom configuration file for yt-dlp",

View File

@@ -18,17 +18,46 @@ import {
} from '@renderer/components/ui/select'
import { Switch } from '@renderer/components/ui/switch'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
import { type LanguageCode, languageList, normalizeLanguageCode } from '@shared/languages'
import type { OneClickQualityPreset } from '@shared/types'
import { useAtom, useSetAtom } from 'jotai'
import { AlertTriangle, CheckCircle2 } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { ipcServices } from '../lib/ipc'
import { logger } from '../lib/logger'
import { loadSettingsAtom, saveSettingAtom, settingsAtom } from '../store/settings'
const normalizeProfileInput = (value: string) => value.trim().replace(/^['"]|['"]$/g, '')
const parseBrowserCookiesSetting = (value: string | undefined) => {
if (!value || value === 'none') {
return { browser: 'none', profile: '' }
}
const separatorIndex = value.indexOf(':')
if (separatorIndex === -1) {
return { browser: value, profile: '' }
}
const browser = value.slice(0, separatorIndex).trim()
const profile = normalizeProfileInput(value.slice(separatorIndex + 1))
return { browser: browser || 'none', profile }
}
const buildBrowserCookiesSetting = (browser: string, profile: string) => {
const trimmedBrowser = browser.trim()
if (!trimmedBrowser || trimmedBrowser === 'none') {
return 'none'
}
const trimmedProfile = normalizeProfileInput(profile)
return trimmedProfile ? `${trimmedBrowser}:${trimmedProfile}` : trimmedBrowser
}
export function Settings() {
const { t, i18n: i18nInstance } = useTranslation()
const { theme, setTheme } = useTheme()
@@ -37,24 +66,20 @@ export function Settings() {
const saveSetting = useSetAtom(saveSettingAtom)
const [platform, setPlatform] = useState<string>('')
const [activeTab, setActiveTab] = useState<string>('general')
const [browserProfileValidation, setBrowserProfileValidation] = useState<{
valid: boolean
reason?: string
}>({ valid: false })
const lastAutoDetectBrowser = useRef<string | null>(null)
useEffect(() => {
logger.info('[Settings] Component mounted, loading settings...')
try {
loadSettings()
// Note: settings will be logged in the next useEffect after it's loaded
} catch (error) {
logger.error('[Settings] Failed to load settings:', error)
}
}, [loadSettings])
useEffect(() => {
logger.info('[Settings] Settings state updated', {
settingsKeys: Object.keys(settings),
settingsValues: settings
})
}, [settings])
useEffect(() => {
const fetchPlatform = async () => {
try {
@@ -70,20 +95,17 @@ export function Settings() {
const autoLaunchSupported = platform === 'darwin' || platform === 'win32'
const handleSettingChange = async (
key: keyof typeof settings,
value: (typeof settings)[keyof typeof settings]
) => {
try {
logger.info('[Settings] Changing setting', { key, value, currentValue: settings[key] })
await saveSetting({ key, value })
toast.success(t('notifications.settingsSaved'))
logger.info('[Settings] Setting changed successfully', { key, value })
} catch (error) {
logger.error('[Settings] Failed to change setting', { key, value, error })
toast.error(t('settings.saveError') || 'Failed to save setting')
}
}
const handleSettingChange = useCallback(
async (key: keyof typeof settings, value: (typeof settings)[keyof typeof settings]) => {
try {
await saveSetting({ key, value })
} catch (error) {
logger.error('[Settings] Failed to change setting', { key, value, error })
toast.error(t('settings.saveError') || 'Failed to save setting')
}
},
[saveSetting, t]
)
const handleSelectPath = async () => {
try {
@@ -146,6 +168,97 @@ export function Settings() {
const activeLanguageCode = normalizeLanguageCode(i18nInstance.language)
const currentLanguage =
languageOptions.find((option) => option.value === activeLanguageCode) ?? languageOptions[0]
const parsedBrowserCookies = parseBrowserCookiesSetting(settings.browserForCookies)
const browserForCookiesValue = parsedBrowserCookies.browser
const browserCookiesProfileValue = parsedBrowserCookies.profile
const normalizedBrowserCookiesSetting = buildBrowserCookiesSetting(
browserForCookiesValue,
browserCookiesProfileValue
)
const hasBrowserProfileValue = browserCookiesProfileValue.trim().length > 0
const showBrowserProfileCheck = hasBrowserProfileValue && browserProfileValidation.valid
const showBrowserProfileWarning = hasBrowserProfileValue && !browserProfileValidation.valid
const getBrowserProfileWarningMessage = (reason?: string) => {
switch (reason) {
case 'pathNotFound':
return t('settings.browserForCookiesProfileInvalidPath')
case 'profileNotFound':
return t('settings.browserForCookiesProfileInvalidProfile')
case 'browserUnsupported':
return t('settings.browserForCookiesProfileInvalidUnsupported')
case 'empty':
return t('settings.browserForCookiesProfileInvalidEmpty')
default:
return t('settings.browserForCookiesProfileInvalid')
}
}
useEffect(() => {
if (settings.browserForCookies !== normalizedBrowserCookiesSetting) {
void handleSettingChange('browserForCookies', normalizedBrowserCookiesSetting)
}
}, [handleSettingChange, normalizedBrowserCookiesSetting, settings.browserForCookies])
useEffect(() => {
const browserChanged = lastAutoDetectBrowser.current !== browserForCookiesValue
const shouldAutoDetect =
browserForCookiesValue !== 'none' && (browserChanged || !browserCookiesProfileValue)
if (!shouldAutoDetect) {
lastAutoDetectBrowser.current = browserForCookiesValue
return
}
const detectProfilePath = async () => {
try {
const detectedPath =
await ipcServices.browserCookies.getBrowserProfilePath(browserForCookiesValue)
const nextProfileValue = detectedPath || ''
if (nextProfileValue !== browserCookiesProfileValue) {
const nextValue = buildBrowserCookiesSetting(browserForCookiesValue, nextProfileValue)
await handleSettingChange('browserForCookies', nextValue)
}
} catch (error) {
logger.error('[Settings] Failed to detect browser profile path:', error)
} finally {
lastAutoDetectBrowser.current = browserForCookiesValue
}
}
void detectProfilePath()
}, [browserForCookiesValue, browserCookiesProfileValue, handleSettingChange])
useEffect(() => {
if (browserForCookiesValue === 'none' || !hasBrowserProfileValue) {
setBrowserProfileValidation({ valid: false, reason: 'empty' })
return
}
let isActive = true
const validateProfilePath = async () => {
try {
const result = await ipcServices.browserCookies.validateBrowserProfilePath(
browserForCookiesValue,
browserCookiesProfileValue
)
if (isActive) {
setBrowserProfileValidation(result)
}
} catch (error) {
if (isActive) {
setBrowserProfileValidation({ valid: false, reason: 'pathNotFound' })
}
logger.error('[Settings] Failed to validate browser profile path:', error)
}
}
void validateProfilePath()
return () => {
isActive = false
}
}, [browserForCookiesValue, browserCookiesProfileValue, hasBrowserProfileValue])
const handleLanguageChange = async (value: LanguageCode) => {
if (activeLanguageCode === value) {
@@ -154,7 +267,6 @@ export function Settings() {
await saveSetting({ key: 'language', value })
await i18nInstance.changeLanguage(value)
toast.success(t('notifications.settingsSaved'))
}
return (
@@ -168,24 +280,7 @@ export function Settings() {
<Tabs
value={activeTab}
onValueChange={(value) => {
logger.info('[Settings] Tab changed', { from: activeTab, to: value })
setActiveTab(value)
try {
if (value === 'advanced') {
logger.info('[Settings] Entering advanced tab', {
settings: settings,
settingsKeys: Object.keys(settings),
maxConcurrentDownloads: settings.maxConcurrentDownloads,
browserForCookies: settings.browserForCookies,
cookiesPath: settings.cookiesPath,
proxy: settings.proxy,
configPath: settings.configPath,
enableAnalytics: settings.enableAnalytics
})
}
} catch (error) {
logger.error('[Settings] Error when entering advanced tab:', error)
}
}}
>
<TabsList className="grid w-full grid-cols-2">
@@ -414,7 +509,6 @@ export function Settings() {
checked={settings.showMoreFormats ?? false}
onCheckedChange={(value) => {
try {
logger.info('[Settings] Toggling showMoreFormats', { value })
handleSettingChange('showMoreFormats', value)
} catch (error) {
logger.error('[Settings] Error toggling showMoreFormats:', error)
@@ -438,22 +532,12 @@ export function Settings() {
try {
const maxConcurrent = settings.maxConcurrentDownloads ?? 5
const maxConcurrentStr = maxConcurrent.toString()
logger.info('[Settings] Rendering max concurrent downloads select', {
maxConcurrent,
maxConcurrentStr,
type: typeof maxConcurrent
})
return (
<Select
value={maxConcurrentStr}
onValueChange={(value) => {
try {
const numValue = Number(value)
logger.info('[Settings] Max concurrent downloads changed', {
oldValue: maxConcurrent,
newValue: numValue,
stringValue: value
})
handleSettingChange('maxConcurrentDownloads', numValue)
} catch (error) {
logger.error(
@@ -497,17 +581,12 @@ export function Settings() {
{(() => {
try {
const proxyValue = settings.proxy ?? ''
logger.info('[Settings] Rendering proxy input', { proxyValue })
return (
<Input
placeholder={t('settings.proxyPlaceholder')}
value={proxyValue}
onChange={(e) => {
try {
logger.info('[Settings] Proxy value changed', {
oldValue: proxyValue,
newValue: e.target.value
})
handleSettingChange('proxy', e.target.value)
} catch (error) {
logger.error('[Settings] Error changing proxy:', error)
@@ -523,48 +602,6 @@ export function Settings() {
})()}
</ItemActions>
</Item>
<ItemSeparator />
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.configFile')}</ItemTitle>
<ItemDescription>{t('settings.configFileDescription')}</ItemDescription>
</ItemContent>
<ItemActions>
{(() => {
try {
const configPathValue = settings.configPath ?? ''
logger.info('[Settings] Rendering config file input', { configPathValue })
return (
<div className="flex gap-2 w-full max-w-md">
<Input value={configPathValue} readOnly className="flex-1" />
<Button onClick={handleSelectConfigFile}>
{t('settings.selectPath')}
</Button>
<Button
variant="secondary"
onClick={() => {
try {
logger.info('[Settings] Clearing config path')
void handleSettingChange('configPath', '')
} catch (error) {
logger.error('[Settings] Error clearing config path:', error)
}
}}
disabled={!configPathValue}
>
{t('settings.clearConfigFile')}
</Button>
</div>
)
} catch (error) {
logger.error('[Settings] Error rendering config file input:', error)
return <div>Error loading config file setting</div>
}
})()}
</ItemActions>
</Item>
</ItemGroup>
<ItemGroup>
@@ -576,20 +613,13 @@ export function Settings() {
<ItemActions>
{(() => {
try {
const browserValue = settings.browserForCookies ?? 'none'
logger.info('[Settings] Rendering browser for cookies select', {
browserValue
})
return (
<Select
value={browserValue}
value={browserForCookiesValue}
onValueChange={(value) => {
try {
logger.info('[Settings] Browser for cookies changed', {
oldValue: browserValue,
newValue: value
})
handleSettingChange('browserForCookies', value)
const nextValue = buildBrowserCookiesSetting(value, '')
handleSettingChange('browserForCookies', nextValue)
} catch (error) {
logger.error('[Settings] Error changing browser for cookies:', error)
}
@@ -618,6 +648,15 @@ export function Settings() {
<SelectItem value="brave">
{t('settings.browserOptions.brave')}
</SelectItem>
<SelectItem value="opera">
{t('settings.browserOptions.opera')}
</SelectItem>
<SelectItem value="vivaldi">
{t('settings.browserOptions.vivaldi')}
</SelectItem>
<SelectItem value="whale">
{t('settings.browserOptions.whale')}
</SelectItem>
</SelectContent>
</Select>
)
@@ -631,6 +670,72 @@ export function Settings() {
<ItemSeparator />
<Item variant="muted">
<ItemContent className="basis-full">
<ItemTitle>{t('settings.browserForCookiesProfile')}</ItemTitle>
<ItemDescription>
{t('settings.browserForCookiesProfileDescription')}
</ItemDescription>
</ItemContent>
<ItemActions className="basis-full">
{(() => {
try {
return (
<div className="relative w-full">
<Input
placeholder={t('settings.browserForCookiesProfilePlaceholder')}
value={browserCookiesProfileValue}
onChange={(event) => {
try {
const newProfileValue = event.target.value
const nextValue = buildBrowserCookiesSetting(
browserForCookiesValue,
newProfileValue
)
handleSettingChange('browserForCookies', nextValue)
} catch (error) {
logger.error(
'[Settings] Error changing browser cookies profile:',
error
)
}
}}
disabled={browserForCookiesValue === 'none'}
className="w-full pr-10"
/>
{showBrowserProfileCheck ? (
<CheckCircle2
className="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-emerald-500"
aria-hidden
/>
) : null}
{showBrowserProfileWarning ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="absolute right-3 top-1/2 inline-flex h-4 w-4 -translate-y-1/2 items-center justify-center text-amber-500">
<AlertTriangle className="h-4 w-4" aria-hidden />
</span>
</TooltipTrigger>
<TooltipContent>
{getBrowserProfileWarningMessage(browserProfileValidation.reason)}
</TooltipContent>
</Tooltip>
) : null}
</div>
)
} catch (error) {
logger.error(
'[Settings] Error rendering browser cookies profile input:',
error
)
return <div>Error loading browser cookies profile setting</div>
}
})()}
</ItemActions>
</Item>
<ItemSeparator />
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.cookiesFile')}</ItemTitle>
@@ -640,7 +745,6 @@ export function Settings() {
{(() => {
try {
const cookiesPathValue = settings.cookiesPath ?? ''
logger.info('[Settings] Rendering cookies file input', { cookiesPathValue })
return (
<div className="flex gap-2 w-full max-w-md">
<Input value={cookiesPathValue} readOnly className="flex-1" />
@@ -651,7 +755,6 @@ export function Settings() {
variant="secondary"
onClick={() => {
try {
logger.info('[Settings] Clearing cookies path')
void handleSettingChange('cookiesPath', '')
} catch (error) {
logger.error('[Settings] Error clearing cookies path:', error)
@@ -687,6 +790,46 @@ export function Settings() {
</Button>
</ItemActions>
</Item>
<ItemSeparator />
<Item variant="muted">
<ItemContent>
<ItemTitle>{t('settings.configFile')}</ItemTitle>
<ItemDescription>{t('settings.configFileDescription')}</ItemDescription>
</ItemContent>
<ItemActions>
{(() => {
try {
const configPathValue = settings.configPath ?? ''
return (
<div className="flex gap-2 w-full max-w-md">
<Input value={configPathValue} readOnly className="flex-1" />
<Button onClick={handleSelectConfigFile}>
{t('settings.selectPath')}
</Button>
<Button
variant="secondary"
onClick={() => {
try {
void handleSettingChange('configPath', '')
} catch (error) {
logger.error('[Settings] Error clearing config path:', error)
}
}}
disabled={!configPathValue}
>
{t('settings.clearConfigFile')}
</Button>
</div>
)
} catch (error) {
logger.error('[Settings] Error rendering config file input:', error)
return <div>Error loading config file setting</div>
}
})()}
</ItemActions>
</Item>
</ItemGroup>
<ItemGroup>
@@ -699,18 +842,11 @@ export function Settings() {
{(() => {
try {
const analyticsValue = settings.enableAnalytics ?? true
logger.info('[Settings] Rendering enable analytics switch', {
analyticsValue
})
return (
<Switch
checked={analyticsValue}
onCheckedChange={(value) => {
try {
logger.info('[Settings] Enable analytics changed', {
oldValue: analyticsValue,
newValue: value
})
handleSettingChange('enableAnalytics', value)
} catch (error) {
logger.error('[Settings] Error changing enable analytics:', error)