mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-04-22 07:58:01 +03:00
feat: Minor 3.1.0 (#150)
* Some features are now optional (#148) * fix(dev): yarn.lock tree fix * feat: bookmarks moved to optional permissions * fix: analytics not working in firefox * feat!: ability to turn off analytics (uses permissions on firefox) * feat: analytics tracker for bookmark export * feat: add privacy policy link in about section * docs: privacy policy update * feat: ability to chain multiple dialogs * fix(loc): analytics option translation * feat: settings review dialog * fix: background script fails to load because of frontend code * chore: use analytics permission as storage value * fix: inverted analytics value * feat!: option to disable thumbnail capture * fix(ci): sed typo * fix: minor fixes * fix(firefox): web-ext lint error fix * chore(ci): switch web-ext action * chore(lint): fix eslint warnings * chore(deps): monthly dependency bump (September 2025) (#149) * chore: 3.1.0 version bump * chore: minor cleanup * fix: allow analytics checkbox stays inactive after denying permission on firefox * fix(deps): yarn.lock rebuild * fix: type assertion for userId * fix: settings review dialog not showing if welcome dialog is not required * fix: analytics and thumbnail capture toggles react incorrectly if permission is denied
This commit is contained in:
+75
-26
@@ -1,5 +1,5 @@
|
||||
import { track, trackError } from "@/features/analytics";
|
||||
import { collectionCount, getCollections, saveCollections } from "@/features/collectionStorage";
|
||||
import { collectionCount, getCollections, thumbnailCaptureEnabled, saveCollections } from "@/features/collectionStorage";
|
||||
import { migrateStorage } from "@/features/migration";
|
||||
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
|
||||
import { SettingsValue } from "@/hooks/useSettings";
|
||||
@@ -13,13 +13,15 @@ import watchTabSelection from "@/utils/watchTabSelection";
|
||||
import { Tabs, Windows } from "wxt/browser";
|
||||
import { Unwatch } from "wxt/storage";
|
||||
import { openCollection, openGroup } from "./sidepanel/utils/opener";
|
||||
import { setSettingsReviewNeeded } from "@/features/settingsReview/utils";
|
||||
import { RemoveListenerCallback } from "@webext-core/messaging";
|
||||
|
||||
export default defineBackground(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const logger = getLogger("background");
|
||||
const graphicsCache: GraphicsStorage = {};
|
||||
let graphicsCache: GraphicsStorage = {};
|
||||
let listLocation: SettingsValue<"listLocation"> = "sidebar";
|
||||
|
||||
logger("Background script started");
|
||||
@@ -36,6 +38,8 @@ export default defineBackground(() =>
|
||||
|
||||
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
|
||||
|
||||
await setSettingsReviewNeeded(reason, previousVersion);
|
||||
|
||||
if (reason === "update" && previousMajor < 3)
|
||||
{
|
||||
await migrateStorage();
|
||||
@@ -44,31 +48,11 @@ export default defineBackground(() =>
|
||||
}
|
||||
});
|
||||
|
||||
browser.tabs.onUpdated.addListener((_, __, tab) =>
|
||||
{
|
||||
if (!tab.url)
|
||||
return;
|
||||
|
||||
graphicsCache[tab.url] = {
|
||||
preview: graphicsCache[tab.url]?.preview,
|
||||
capture: graphicsCache[tab.url]?.capture,
|
||||
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
|
||||
};
|
||||
});
|
||||
|
||||
browser.commands.onCommand.addListener(
|
||||
(command, tab) => performContextAction(command, tab!.windowId!)
|
||||
);
|
||||
|
||||
onMessage("getGraphicsCache", () => graphicsCache);
|
||||
onMessage("addThumbnail", ({ data }) =>
|
||||
{
|
||||
graphicsCache[data.url] = {
|
||||
preview: data.thumbnail,
|
||||
capture: graphicsCache[data.url]?.capture,
|
||||
icon: graphicsCache[data.url]?.icon
|
||||
};
|
||||
});
|
||||
onMessage("refreshCollections", () => { });
|
||||
|
||||
if (import.meta.env.FIREFOX)
|
||||
@@ -80,6 +64,21 @@ export default defineBackground(() =>
|
||||
setupTabCaputre();
|
||||
async function setupTabCaputre(): Promise<void>
|
||||
{
|
||||
let unwatchAddThumbnail: RemoveListenerCallback | null = null;
|
||||
let captureInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const captureFavicon = (_: any, __: any, tab: Tabs.Tab): void =>
|
||||
{
|
||||
if (!tab.url)
|
||||
return;
|
||||
|
||||
graphicsCache[tab.url] = {
|
||||
preview: graphicsCache[tab.url]?.preview,
|
||||
capture: graphicsCache[tab.url]?.capture,
|
||||
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
|
||||
};
|
||||
};
|
||||
|
||||
const tryCaptureTab = async (tab: Tabs.Tab): Promise<void> =>
|
||||
{
|
||||
if (!tab.url || tab.status !== "complete" || !tab.active)
|
||||
@@ -113,11 +112,61 @@ export default defineBackground(() =>
|
||||
}
|
||||
};
|
||||
|
||||
setInterval(() =>
|
||||
const updateCapture = async (captureThumbnails: boolean): Promise<void> =>
|
||||
{
|
||||
browser.tabs.query({ active: true })
|
||||
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
|
||||
}, 1000);
|
||||
const scriptingGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"] });
|
||||
|
||||
if (captureThumbnails)
|
||||
{
|
||||
if (scriptingGranted)
|
||||
await browser.scripting.registerContentScripts([
|
||||
{
|
||||
id: "capture-script",
|
||||
matches: ["<all_urls>"],
|
||||
runAt: "document_idle",
|
||||
js: ["capture.js"]
|
||||
}
|
||||
]);
|
||||
|
||||
unwatchAddThumbnail = onMessage("addThumbnail", ({ data }) =>
|
||||
{
|
||||
graphicsCache[data.url] = {
|
||||
preview: data.thumbnail,
|
||||
capture: graphicsCache[data.url]?.capture,
|
||||
icon: graphicsCache[data.url]?.icon
|
||||
};
|
||||
});
|
||||
|
||||
captureInterval = setInterval(() =>
|
||||
{
|
||||
browser.tabs.query({ active: true })
|
||||
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
|
||||
}, 1000);
|
||||
|
||||
browser.tabs.onUpdated.addListener(captureFavicon);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (scriptingGranted)
|
||||
await browser.scripting.unregisterContentScripts({
|
||||
ids: ["capture-script"]
|
||||
});
|
||||
|
||||
unwatchAddThumbnail?.();
|
||||
|
||||
if (captureInterval)
|
||||
clearInterval(captureInterval);
|
||||
|
||||
browser.tabs.onUpdated.removeListener(captureFavicon);
|
||||
|
||||
graphicsCache = {};
|
||||
}
|
||||
};
|
||||
|
||||
if (await thumbnailCaptureEnabled.getValue())
|
||||
updateCapture(true);
|
||||
|
||||
thumbnailCaptureEnabled.watch(updateCapture);
|
||||
}
|
||||
|
||||
setupContextMenu();
|
||||
|
||||
@@ -4,11 +4,7 @@ import { sendMessage } from "@/utils/messaging";
|
||||
// This content script is injected into each browser tab.
|
||||
// It's purpose is to retrive an OpenGraph thumbnail URL from the metadata
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ["<all_urls>"],
|
||||
runAt: "document_idle",
|
||||
main
|
||||
});
|
||||
export default defineUnlistedScript({ main });
|
||||
|
||||
const logger = getLogger("contentScript");
|
||||
|
||||
@@ -34,5 +34,12 @@ export const useOptionsStyles = makeStyles({
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: tokens.spacingHorizontalS
|
||||
},
|
||||
group:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: tokens.spacingVerticalSNudge
|
||||
}
|
||||
});
|
||||
|
||||
@@ -34,7 +34,8 @@ export default function AboutSection(): React.ReactElement
|
||||
<Body1 as="p">
|
||||
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link>
|
||||
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link><br />
|
||||
<Link { ...extLink(githubLinks.privacy) }>{ i18n.t("options_page.about.links.privacy") }</Link>
|
||||
</Body1>
|
||||
|
||||
<div className={ cls.horizontalButtons }>
|
||||
|
||||
@@ -2,6 +2,7 @@ import useSettings, { SettingsValue } from "@/hooks/useSettings";
|
||||
import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
|
||||
import { KeyCommand20Regular } from "@fluentui/react-icons";
|
||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
|
||||
export default function GeneralSection(): React.ReactElement
|
||||
{
|
||||
@@ -14,8 +15,23 @@ export default function GeneralSection(): React.ReactElement
|
||||
const [listLocation, setListLocation] = useSettings("listLocation");
|
||||
const [contextAction, setContextAction] = useSettings("contextAction");
|
||||
|
||||
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
|
||||
|
||||
const cls = useOptionsStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
analyticsPermission.getValue().then(setAllowAnalytics);
|
||||
return analyticsPermission.watch(setAllowAnalytics);
|
||||
}, []);
|
||||
|
||||
const updateAnalytics = (enabled: boolean): void =>
|
||||
{
|
||||
setAllowAnalytics(null);
|
||||
analyticsPermission.setValue(enabled)
|
||||
.catch(() => setAllowAnalytics(!enabled));
|
||||
};
|
||||
|
||||
const openShortcutsPage = (): Promise<any> =>
|
||||
browser.tabs.create({
|
||||
url: "chrome://extensions/shortcuts",
|
||||
@@ -60,6 +76,12 @@ export default function GeneralSection(): React.ReactElement
|
||||
label={ i18n.t("options_page.general.options.unload_tabs") }
|
||||
checked={ dismissOnLoad ?? false }
|
||||
onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } />
|
||||
|
||||
<Checkbox
|
||||
label={ i18n.t("options_page.general.options.allow_analytics") }
|
||||
checked={ allowAnalytics ?? true }
|
||||
disabled={ allowAnalytics === null }
|
||||
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
|
||||
</section>
|
||||
|
||||
<Field label={ i18n.t("options_page.general.options.list_locations.title") }>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { cloudDisabled, setCloudStorage } from "@/features/collectionStorage";
|
||||
import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||
import { Button, Field, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar } from "@fluentui/react-components";
|
||||
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components";
|
||||
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
|
||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||
import exportData from "../utils/exportData";
|
||||
import importData from "../utils/importData";
|
||||
import { Unwatch } from "wxt/storage";
|
||||
|
||||
export default function StorageSection(): React.ReactElement
|
||||
{
|
||||
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
|
||||
const [importResult, setImportResult] = useState<boolean | null>(null);
|
||||
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
|
||||
const [isThumbnailCaptureEnabled, setThumbnailCaptureEnabled] = useState<boolean | null>(null);
|
||||
|
||||
const dialog = useDialog();
|
||||
const cls = useOptionsStyles();
|
||||
@@ -20,10 +22,35 @@ export default function StorageSection(): React.ReactElement
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
thumbnailCaptureEnabled.getValue().then(setThumbnailCaptureEnabled);
|
||||
cloudDisabled.getValue().then(setCloudDisabled);
|
||||
return cloudDisabled.watch(setCloudDisabled);
|
||||
|
||||
const unwatchCloud: Unwatch = cloudDisabled.watch(setCloudDisabled);
|
||||
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setThumbnailCaptureEnabled);
|
||||
|
||||
return () =>
|
||||
{
|
||||
unwatchCloud();
|
||||
unwatchThumbnails();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSetThumbnailCapture = (enabled: boolean): void =>
|
||||
{
|
||||
setThumbnailCaptureEnabled(null);
|
||||
thumbnailCaptureEnabled.setValue(enabled)
|
||||
.catch(() => setThumbnailCaptureEnabled(!enabled));
|
||||
};
|
||||
|
||||
const handleClearThumbnails = (): void =>
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("options_page.storage.clear_thumbnails.title"),
|
||||
content: i18n.t("options_page.storage.clear_thumbnails.prompt"),
|
||||
confirmText: i18n.t("common.actions.delete"),
|
||||
destructive: true,
|
||||
onConfirm: () => clearGraphicsStorage()
|
||||
});
|
||||
|
||||
const handleImport = (): void =>
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("options_page.storage.import_prompt.title"),
|
||||
@@ -51,6 +78,29 @@ export default function StorageSection(): React.ReactElement
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={ cls.group }>
|
||||
<Switch
|
||||
checked={ isThumbnailCaptureEnabled ?? true }
|
||||
disabled={ isThumbnailCaptureEnabled === null }
|
||||
onChange={ (_, e) => handleSetThumbnailCapture(e.checked as boolean) }
|
||||
label={ {
|
||||
children: (_: any, props: LabelProps) =>
|
||||
<InfoLabel
|
||||
{ ...props }
|
||||
label={ i18n.t("options_page.storage.thumbnail_capture") }
|
||||
info={
|
||||
<p>
|
||||
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }<br /><br />
|
||||
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
|
||||
</p>
|
||||
} />
|
||||
} } />
|
||||
|
||||
<Button onClick={ handleClearThumbnails } className={ dangerCls.buttonSubtle } appearance="subtle">
|
||||
{ i18n.t("options_page.storage.clear_thumbnails.action") }
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{ isCloudDisabled === false &&
|
||||
<Field
|
||||
label={ i18n.t("options_page.storage.capacity.title") }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import App from "@/App.tsx";
|
||||
import "@/assets/global.css";
|
||||
import { trackPage } from "@/features/analytics";
|
||||
import { Tab, TabList } from "@fluentui/react-components";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
|
||||
@@ -14,7 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
</App>
|
||||
);
|
||||
|
||||
analytics.page("options_page");
|
||||
trackPage("options_page");
|
||||
|
||||
function OptionsPage(): React.ReactElement
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import App from "@/App.tsx";
|
||||
import "@/assets/global.css";
|
||||
import { trackPage } from "@/features/analytics";
|
||||
import { useLocalMigration } from "@/features/migration";
|
||||
import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog";
|
||||
import { Divider, makeStyles } from "@fluentui/react-components";
|
||||
@@ -7,6 +8,8 @@ import ReactDOM from "react-dom/client";
|
||||
import CollectionsProvider from "./contexts/CollectionsProvider";
|
||||
import CollectionListView from "./layouts/collections/CollectionListView";
|
||||
import Header from "./layouts/header/Header";
|
||||
import { useSettingsReviewDialog } from "@/features/settingsReview";
|
||||
import useDialogTrain from "@/hooks/useDialogTrain";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<App>
|
||||
@@ -15,14 +18,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
);
|
||||
|
||||
document.title = i18n.t("manifest.name");
|
||||
analytics.page("collection_list");
|
||||
trackPage("collection_list");
|
||||
|
||||
function MainPage(): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
useLocalMigration();
|
||||
useWelcomeDialog();
|
||||
useDialogTrain(
|
||||
useWelcomeDialog,
|
||||
useSettingsReviewDialog
|
||||
);
|
||||
|
||||
return (
|
||||
<CollectionsProvider>
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { Bookmarks } from "wxt/browser";
|
||||
import { Bookmarks, Permissions } from "wxt/browser";
|
||||
import { getCollectionTitle } from "./getCollectionTitle";
|
||||
import { track } from "@/features/analytics";
|
||||
|
||||
export default async function exportCollectionToBookmarks(collection: CollectionItem)
|
||||
{
|
||||
const permissions: Permissions.AnyPermissions = await browser.permissions.getAll();
|
||||
|
||||
if (!permissions.permissions?.includes("bookmarks"))
|
||||
{
|
||||
const granted: boolean = await browser.permissions.request({ permissions: ["bookmarks"] });
|
||||
|
||||
if (!granted)
|
||||
return;
|
||||
}
|
||||
|
||||
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
||||
title: getCollectionTitle(collection)
|
||||
});
|
||||
@@ -31,6 +42,8 @@ export default async function exportCollectionToBookmarks(collection: Collection
|
||||
}
|
||||
}
|
||||
|
||||
track("bookmarks_saved");
|
||||
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.bookmark_saved.title"),
|
||||
message: i18n.t("notifications.bookmark_saved.message"),
|
||||
|
||||
Reference in New Issue
Block a user