import React, { ReactElement, useEffect, useState } from 'react'; import { toNumber } from 'lodash'; import { useLiveQuery } from 'dexie-react-hooks'; import { getBackupEveryNSeconds, setBackupEveryNSeconds, getBackupDirectoryHandle, getBackupPath, setBackupPath, getLatestBackup, getLatestBackupSize, getIsBackupOverdue, getBackupStatus, BackupStatus, getIgnoredUrls, setIgnoredUrls, getNormalizeRules, setNormalizeRules, getSimilarityScaler, setSimilarityScaler, getTagSimilarityScaler, setTagSimilarityScaler, } from '../db/prefs'; import { loadPrefsFromDB } from '../db/prefsLoader'; import { db } from '../db/db'; import { maybeBackup, setBackupDir, restore, requestBackupPermission } from '../db/backup'; import { enqueueOperation } from '../db/queue'; import { useBackgroundProgress } from '../db/progress'; import { Button, CssBaseline, CssVarsProvider, FormControl, FormLabel, Input, Sheet, Typography, Stack, Divider, Alert, IconButton, Box, Slider, LinearProgress, Modal, ModalDialog, DialogTitle, DialogContent, DialogActions, } from '@mui/joy'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faDownload, faFolderOpen, faTriangleExclamation, faSave, faTrash, faPlus, faUpload, faBug, } from '@fortawesome/free-solid-svg-icons'; import '@fontsource/inter'; const Options = (): ReactElement => { const backupEveryNSeconds = useLiveQuery(getBackupEveryNSeconds); const isBackupOverdue = useLiveQuery(getIsBackupOverdue); const backupStatus = useLiveQuery(getBackupStatus); const latestBackup = useLiveQuery(getLatestBackup); const latestBackupSize = useLiveQuery(getLatestBackupSize); const backupDirHandle = useLiveQuery(getBackupDirectoryHandle); const dbBackupPath = useLiveQuery(getBackupPath); const ignoredUrls = useLiveQuery(getIgnoredUrls); const normalizeRules = useLiveQuery(getNormalizeRules); const similarityScaler = useLiveQuery(getSimilarityScaler); const tagSimilarityScaler = useLiveQuery(getTagSimilarityScaler); const [localIgnoredUrls, setLocalIgnoredUrls] = useState([]); const [localNormalizeRules, setLocalNormalizeRules] = useState<[string, string][]>([]); const [saveStatus, setSaveStatus] = useState(''); const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); const [isRestoring, setIsRestoring] = useState(false); const handleRestore = async () => { try { setIsRestoring(true); await restore(); setSaveStatus('Backup restored successfully! Restarting...'); setIsRestoreModalOpen(false); setTimeout(() => { if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.reload) { chrome.runtime.reload(); } else { window.location.reload(); } }, 2000); } catch (e: any) { if (e.name !== 'AbortError') { setSaveStatus('Error restoring backup: ' + e.message); } setIsRestoreModalOpen(false); } finally { setIsRestoring(false); } }; // Ensure DB is initialized useEffect(() => { loadPrefsFromDB(); }, []); // Sync from DB to local state when db data loads useEffect(() => { if (ignoredUrls !== undefined) { setLocalIgnoredUrls(ignoredUrls); } }, [ignoredUrls]); useEffect(() => { if (normalizeRules !== undefined) { setLocalNormalizeRules(normalizeRules); } }, [normalizeRules]); const handleSave = async () => { try { await setIgnoredUrls(localIgnoredUrls.filter(u => u.trim() !== '')); await setNormalizeRules(localNormalizeRules.filter(r => r[0].trim() !== '')); // Reload in this process await loadPrefsFromDB(); // Notify background await chrome.runtime.sendMessage({ type: 'reloadPrefs' }).catch(() => {}); setSaveStatus('Saved!'); setTimeout(() => setSaveStatus(''), 2000); } catch (e: any) { setSaveStatus('Error: ' + e.message); } }; const handleClearLogs = async () => { try { setSaveStatus('Clearing logs...'); await db.logs.clear(); setSaveStatus('Logs cleared!'); setTimeout(() => setSaveStatus(''), 2000); } catch (e: any) { setSaveStatus('Error clearing logs: ' + e.message); } }; const handleDownloadLogs = async () => { try { setSaveStatus('Fetching logs...'); const logs = await db.logs.orderBy('timestamp').reverse().limit(1000).toArray(); const text = logs.map( l => `[${new Date(l.timestamp).toISOString()}] ${l.level.toUpperCase()} ${l.context} [Mem: ${l.memoryUsage ? Math.round(l.memoryUsage / 1024 / 1024) + 'MB' : 'N/A'}] - ${l.message}` ).join('\n'); const blob = new Blob([text], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `tabvana-debug-logs-${new Date().toISOString()}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); setSaveStatus('Logs downloaded!'); setTimeout(() => setSaveStatus(''), 2000); } catch (e: any) { setSaveStatus('Error getting logs: ' + e.message); } }; const addIgnoredUrl = () => { setLocalIgnoredUrls([...localIgnoredUrls, '']); }; const removeIgnoredUrl = (index: number) => { const newUrls = [...localIgnoredUrls]; newUrls.splice(index, 1); setLocalIgnoredUrls(newUrls); }; const updateIgnoredUrl = (index: number, value: string) => { const newUrls = [...localIgnoredUrls]; newUrls[index] = value; setLocalIgnoredUrls(newUrls); }; const addNormalizeRule = () => { setLocalNormalizeRules([...localNormalizeRules, ['', '$1']]); }; const removeNormalizeRule = (index: number) => { const newRules = [...localNormalizeRules]; newRules.splice(index, 1); setLocalNormalizeRules(newRules); }; const updateNormalizeRule = (index: number, field: 0 | 1, value: string) => { const newRules = [...localNormalizeRules]; newRules[index][field] = value; setLocalNormalizeRules(newRules); }; return ( Tabvana Options Visual Settings Page Similarity Scaler: {similarityScaler} setSimilarityScaler(val as number)} /> Tag Similarity Scaler: {tagSimilarityScaler} setTagSimilarityScaler(val as number)} /> Backup Settings Backup Interval (seconds) { setBackupEveryNSeconds(toNumber(event.target.value)); }} /> Current Backup Directory:{' '} {dbBackupPath ? ( {dbBackupPath} ) : ( backupDirHandle?.name || 'Not set' )} {latestBackup && ( Last backup: {new Date(latestBackup).toLocaleString()} ({formatBytes(latestBackupSize || 0)}) )} !isRestoring && setIsRestoreModalOpen(false)}> Confirmation Are you sure you want to restore a backup? This will completely replace your current history and settings, and cannot be undone. Configuration Ignored URLs (Exact substring match) {localIgnoredUrls.map((url, index) => ( updateIgnoredUrl(index, e.target.value)} placeholder="e.g. chrome://" /> removeIgnoredUrl(index)} > ))} Normalization Rules (Regex Capture {`\u2192`} Replacement) {localNormalizeRules.map((rule, index) => ( updateNormalizeRule(index, 0, e.target.value)} placeholder="Regex Pattern (e.g. (.*)\?.*)" /> {`\u2192`} updateNormalizeRule(index, 1, e.target.value)} placeholder="Replacement (e.g. $1)" /> removeNormalizeRule(index)} > ))} {saveStatus && ( {saveStatus} )} Advanced Maintenance } variant="soft" color="warning" size="sm" > Renormalizing the database rebuilds search indices. This operation can be slow for large histories. Diagnostics If you experience crashes, download the internal error logs to debug the cause. ); }; const formatBytes = (bytes: number, decimals: number = 2) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }; export default Options;