feat(settings): improve cookie profile handling (#74)
* feat(settings): improve cookie profile handling * fix(settings): normalize profile input
This commit is contained in:
@@ -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,
|
||||
|
||||
270
src/main/ipc/services/browser-cookies-service.ts
Normal file
270
src/main/ipc/services/browser-cookies-service.ts
Normal 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 }
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user