Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions components/options/BackupSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useRef, useState } from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { useSettingsStore } from '../../store/settings';
import { exportSettingsJson, parseImportedSettings } from '../../utils/settings-transfer';

export function BackupSection() {
const t = useTranslation();
const settings = useSettingsStore();
const [importError, setImportError] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);

function handleExport() {
const json = exportSettingsJson(settings);
const url = URL.createObjectURL(new Blob([json], { type: 'application/json' }));
const a = document.createElement('a');
a.href = url;
a.download = 'bleep-settings.json';
a.click();
URL.revokeObjectURL(url);
}

async function handleImportFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) return;
try {
const data = parseImportedSettings(await file.text());
settings.importSettings(data);
setImportError(false);
} catch (err) {
console.error('Bleep: settings import failed', err);
setImportError(true);
}
}

return (
<div className="mb-3 max-w-xl">
<label className="block text-sm mb-1">{t('backupTitle')}</label>
<p className="text-xs text-stone-500 mb-2">{t('backupHint')}</p>
<div className="flex gap-2">
<button
onClick={handleExport}
className="rounded-md border border-stone-300 hover:bg-stone-100 dark:border-stone-600 dark:hover:bg-stone-700 cursor-pointer px-3 py-1.5 text-xs"
>
{t('exportSettings')}
</button>
<button
onClick={() => fileInputRef.current?.click()}
className="rounded-md border border-stone-300 hover:bg-stone-100 dark:border-stone-600 dark:hover:bg-stone-700 cursor-pointer px-3 py-1.5 text-xs"
>
{t('importSettings')}
</button>
<input
ref={fileInputRef}
type="file"
accept="application/json"
className="hidden"
onChange={handleImportFile}
/>
</div>
{importError && <p className="text-xs text-red-500 mt-1">{t('importInvalidFile')}</p>}
</div>
);
}
48 changes: 48 additions & 0 deletions components/options/ClearLogSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useTranslation } from '../../hooks/useTranslation';
import { useClearLogStore } from '../../store/clearLog';
import type { TranslationKey } from '../../utils/i18n';

export function ClearLogSection() {
const t = useTranslation();
const entries = useClearLogStore((s) => s.entries);
const clearLog = useClearLogStore((s) => s.clearLog);

return (
<div className="mt-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm uppercase tracking-wide text-stone-500 dark:text-stone-400">
{t('clearLogTitle')}
</h3>
{entries.length > 0 && (
<button
onClick={clearLog}
className="text-xs text-stone-500 hover:text-stone-900 dark:hover:text-stone-100 cursor-pointer underline"
>
{t('clearLogClear')}
</button>
)}
</div>
{entries.length === 0 ? (
<p className="text-xs text-stone-500">{t('clearLogEmpty')}</p>
) : (
<ul className="divide-y divide-stone-200 rounded-md border border-stone-200 dark:divide-stone-700 dark:border-stone-700 max-h-60 overflow-y-auto">
{entries.map((entry) => (
<li key={entry.id} className="px-3 py-2">
<div className="flex items-center justify-between gap-2">
<span className="text-sm truncate">{entry.hostname}</span>
<span className="text-xs text-stone-500 shrink-0">
{new Date(entry.timestamp).toLocaleString()}
</span>
</div>
<p className="text-xs text-stone-500 truncate">
{entry.types.map((id) => t(`dt_${id}` as TranslationKey)).join(', ')}
{entry.linkedTargets.length > 0 &&
` + ${t('clearLogLinkedCount', { count: entry.linkedTargets.length })}`}
</p>
</li>
))}
</ul>
)}
</div>
);
}
3 changes: 3 additions & 0 deletions components/options/GlobalSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface GlobalSectionProps {
historyStatus: ClearStatus;
onClearAllHistory: () => void;
onDeleteHistoryItem: (url: string) => void;
hasProtectedSites: boolean;
}

export function GlobalSection({
Expand All @@ -32,6 +33,7 @@ export function GlobalSection({
historyStatus,
onClearAllHistory,
onDeleteHistoryItem,
hasProtectedSites,
}: GlobalSectionProps) {
const t = useTranslation();

Expand All @@ -51,6 +53,7 @@ export function GlobalSection({
onConfirm={onClear}
/>
</div>
{hasProtectedSites && <p className="text-xs text-stone-500 mt-2">{t('protectedSitesGlobalNote')}</p>}

{showHistory && (
<HistorySection
Expand Down
7 changes: 7 additions & 0 deletions components/options/PerSiteSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { VisitedSite } from '../../utils/clearing';
import { DataTypeGrid } from '../DataTypeGrid';
import { OriginMappingsEditor } from '../OriginMappingsEditor';
import { type ClearStatus } from '../StatusButton';
import { ProtectedSitesSection } from './ProtectedSitesSection';
import { SiteTabsSection } from './SiteTabsSection';
import { VisitedSitesSection } from './VisitedSitesSection';

Expand All @@ -19,6 +20,8 @@ interface PerSiteSectionProps {
onUseOriginMappingsChange: (value: boolean) => void;
linkedOrigins: string;
onLinkedOriginsChange: (value: string) => void;
protectedSites: string;
onProtectedSitesChange: (value: string) => void;
siteFilter: string;
onSiteFilterChange: (value: string) => void;
bulkStatus: ClearStatus;
Expand Down Expand Up @@ -48,6 +51,8 @@ export function PerSiteSection({
onUseOriginMappingsChange,
linkedOrigins,
onLinkedOriginsChange,
protectedSites,
onProtectedSitesChange,
siteFilter,
onSiteFilterChange,
bulkStatus,
Expand Down Expand Up @@ -104,6 +109,8 @@ export function PerSiteSection({
<OriginMappingsEditor value={linkedOrigins} onChange={onLinkedOriginsChange} />
</div>

<ProtectedSitesSection value={protectedSites} onChange={onProtectedSitesChange} />

<hr className="border-stone-200 dark:border-stone-700 mb-3" />

{!isGeckoBased() && (
Expand Down
24 changes: 24 additions & 0 deletions components/options/ProtectedSitesSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useTranslation } from '../../hooks/useTranslation';

interface ProtectedSitesSectionProps {
value: string;
onChange: (value: string) => void;
}

export function ProtectedSitesSection({ value, onChange }: ProtectedSitesSectionProps) {
const t = useTranslation();

return (
<div className="mb-3 max-w-xl">
<label className="block text-sm mb-1">{t('protectedSites')}</label>
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={t('protectedSitesPlaceholder')}
rows={2}
className="w-full rounded-md border border-stone-300 bg-stone-50 dark:border-stone-600 dark:bg-stone-800 px-3 py-1.5 text-sm placeholder:text-stone-500 focus:outline-none focus:border-blue-500 resize-y"
/>
<p className="text-xs text-stone-500 mt-2">{t('protectedSitesHint')}</p>
</div>
);
}
12 changes: 12 additions & 0 deletions components/options/VisitedSitesSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useEffect, useState } from 'react';
import { useTranslation } from '../../hooks/useTranslation';
import { getBrowserName, isGeckoBased } from '../../utils/browser-info';
import type { VisitedSite } from '../../utils/clearing';
import { DangerConfirmButton } from '../DangerConfirmButton';
import { type ClearStatus } from '../StatusButton';
Expand Down Expand Up @@ -26,13 +28,23 @@ export function VisitedSitesSection({
onClearSite,
}: VisitedSitesSectionProps) {
const t = useTranslation();
const [browserName, setBrowserName] = useState<string | null>(null);

useEffect(() => {
if (isGeckoBased()) getBrowserName().then(setBrowserName);
}, []);

return (
<div className="mt-4">
<h3 className="text-sm uppercase tracking-wide text-stone-500 dark:text-stone-400 mb-2">
{t('visitedSites')}
</h3>
<p className="text-xs text-stone-500 mb-2">{t('visitedSitesDescription')}</p>
{browserName && (
<p className="text-xs text-stone-500 mb-2">
{t('visitedSitesContainerCaveat', { browser: browserName })}
</p>
)}

<div className="flex gap-2 mb-2">
<input
Expand Down
56 changes: 56 additions & 0 deletions entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { browser } from 'wxt/browser';
import { defineBackground } from 'wxt/utils/define-background';
import { linkedOriginsFor, tabOrigin } from '../utils/clearing';
import { readPersistedSettingsRaw } from '../utils/storage-adapter';

const BADGE_COLOR = '#2563eb';
const SETTINGS_KEY = 'cache-cleaner-settings';

interface PersistedShape {
state?: { linkedOrigins?: string; useOriginMappings?: boolean };
}

async function readMappingSettings(): Promise<{ linkedOrigins: string; useOriginMappings: boolean }> {
const raw = (await readPersistedSettingsRaw(SETTINGS_KEY)) as PersistedShape | null;
return {
linkedOrigins: raw?.state?.linkedOrigins ?? '',
useOriginMappings: raw?.state?.useOriginMappings ?? false,
};
}

async function updateBadge(tabId: number, url: string | undefined) {
const origin = url ? tabOrigin({ url }) : null;
if (!origin) {
await browser.action.setBadgeText({ tabId, text: '' });
return;
}

const { linkedOrigins, useOriginMappings } = await readMappingSettings();
const hasMapping = useOriginMappings && linkedOriginsFor(linkedOrigins, origin).length > 0;

await browser.action.setBadgeText({ tabId, text: hasMapping ? '●' : '' });
if (hasMapping) {
await browser.action.setBadgeBackgroundColor({ tabId, color: BADGE_COLOR });
}
}

export default defineBackground(() => {
browser.tabs.onActivated.addListener(({ tabId }) => {
browser.tabs.get(tabId).then((tab) => updateBadge(tabId, tab.url));
});

browser.tabs.onUpdated.addListener((tabId, info, tab) => {
if (info.status === 'complete') updateBadge(tabId, tab.url);
});

// Editing the mapping list or toggling "Use origin mappings" should update the
// badge immediately, not just on the next tab switch/navigation.
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'sync' || !Object.keys(changes).some((key) => key.startsWith(SETTINGS_KEY))) return;
browser.tabs.query({ active: true }).then((tabs) => {
for (const tab of tabs) {
if (tab.id != null) updateBadge(tab.id, tab.url);
}
});
});
});
Loading
Loading