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:
@@ -59,6 +59,11 @@ jobs:
|
||||
working-directory: ./node_modules/@dnd-kit/core/dist
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
|
||||
# Patch for firefox analytics (see https://github.com/wxt-dev/wxt/pull/1808)
|
||||
- run: sed -i 's|if (location.pathname === "/background.js")|if (location.pathname === "/background.js" \|\| location.pathname === "/_generated_background_page.html")|' index.mjs
|
||||
working-directory: ./node_modules/@wxt-dev/analytics/dist
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
|
||||
- run: yarn zip -b ${{ matrix.target }}
|
||||
|
||||
- name: Drop build artifacts (${{ matrix.target }})
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@main
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
extver=`jq -r ".version" package.json`
|
||||
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: dev-build-deploy/release-me@v0.18.0
|
||||
- uses: dev-build-deploy/release-me@v0.18.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: v
|
||||
|
||||
@@ -51,6 +51,11 @@ jobs:
|
||||
working-directory: ./node_modules/@dnd-kit/core/dist
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
|
||||
# Patch for firefox analytics (see https://github.com/wxt-dev/wxt/pull/1808)
|
||||
- run: sed -i 's|if (location.pathname === "/background.js")|if (location.pathname === "/background.js" \|\| location.pathname === "/_generated_background_page.html")|' index.mjs
|
||||
working-directory: ./node_modules/@wxt-dev/analytics/dist
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
|
||||
- run: yarn zip -b ${{ matrix.target }}
|
||||
|
||||
- name: Drop artifacts (${{ matrix.target }})
|
||||
@@ -62,9 +67,10 @@ jobs:
|
||||
|
||||
- name: web-ext lint
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
uses: freaktechnik/web-ext-lint@main
|
||||
uses: kewisch/action-web-ext@main
|
||||
with:
|
||||
extension-root: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
cmd: lint
|
||||
source: ./.output/firefox-mv3
|
||||
channel: listed
|
||||
|
||||
- run: yarn npm audit
|
||||
|
||||
+35
-2
@@ -6,7 +6,7 @@
|
||||
- Thumbnails of saved tabs
|
||||
3. This extension uses Google Analytics to collect usage statistics and improve the extension.
|
||||
4. This extension uses analytics to collect following data:
|
||||
- Random UUID to identify the user
|
||||
- Random UUID to distinguish unique users
|
||||
- Browser name and version
|
||||
- Operating system name and version
|
||||
- System architecture
|
||||
@@ -14,7 +14,40 @@
|
||||
- Extension language
|
||||
- User settings
|
||||
- Number of saved collections
|
||||
- Action identifiers (e.g. "page_view", "extension_installed", "item_created", etc.)
|
||||
- Events, related to user's actions:
|
||||
- `bmc_clicked` (when "Buy me a Coffee" button is clicked)
|
||||
- `collection_list` (when extension's options page is opened)
|
||||
- `cta_dismissed` (when "Like this extension?" prompt is closed)
|
||||
- `extension_installed` (when extension is installed or updated)
|
||||
- `feedback_clicked` (when "Leave feedback" button is clicked)
|
||||
- `item_created` (when new collection or group is created using dialog window)
|
||||
- `item_edited` (when collection or group is edited)
|
||||
- `options_page` (when extension's options page is opened)
|
||||
- `page_view` (when extension's page is opened)
|
||||
- `save` (when "Save all tabs" or "Save selected tabs" buttons are clicked)
|
||||
- `set_aside` (when "Set all tabs aside" or "Set selected tabs aside" buttons are clicked)
|
||||
- `used_drag_and_drop` (when items inside collection list were reordered)
|
||||
- `visit_blog_button_click` (when "Read dev blog" button is clicked)
|
||||
- `bookmarks_saved` (when "Export to bookmarks" option is clicked)
|
||||
- Events, related to extension errors:
|
||||
- `background_error` (when error inside background service has occured)
|
||||
- `cloud_get_error` (when failed to retrieve collections from the cloud storage)
|
||||
- `conflict_resolve_with_cloud_error` (when failed to retrieve collections from the cloud storage during storage conflict resolution)
|
||||
- `cloud_save_error` (when failed to save collections to the cloud storage)
|
||||
- `messaging_error` (when failed to send a message to extenion's background service)
|
||||
- `notification_error` (when failed to display a toast notification)
|
||||
4. Following events, beside their name, include additional information, such as:
|
||||
- `item_created` and `item_edited`:
|
||||
- Type of the affected item (`collection` or `group`)
|
||||
- `extension_installed`:
|
||||
- Reason for update (`install`, `update`, or `browser_update`)
|
||||
- Previously installed extension's version, if applicable
|
||||
- `page_view`:
|
||||
- Type of the page (`options_page` or `collection_list`)
|
||||
- All extension's error events:
|
||||
- Error name
|
||||
- Error message
|
||||
- Error call stack
|
||||
4. This extension does not collect or use any personally identifiable information (PII) or sensitive data for purposes other than its core functionality.
|
||||
5. This extension uses cloud storage built into your browser to store its data.
|
||||
6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
|
||||
import { WxtAppConfig } from "wxt/sandbox";
|
||||
import { userPropertiesStorage } from "./features/analytics";
|
||||
|
||||
export default defineAppConfig({
|
||||
analytics:
|
||||
{
|
||||
debug: import.meta.env.DEV,
|
||||
enabled: storage.defineItem("local:analytics", {
|
||||
fallback: true
|
||||
}),
|
||||
userId: storage.defineItem("local:userId", {
|
||||
init: () => crypto.randomUUID()
|
||||
}),
|
||||
userProperties: userPropertiesStorage,
|
||||
providers:
|
||||
[
|
||||
googleAnalytics4({
|
||||
apiSecret: import.meta.env.WXT_GA4_API_SECRET,
|
||||
measurementId: import.meta.env.WXT_GA4_MEASUREMENT_ID
|
||||
})
|
||||
]
|
||||
}
|
||||
} as WxtAppConfig);
|
||||
+2
-1
@@ -13,7 +13,8 @@ export const githubLinks =
|
||||
repo: githubLink(),
|
||||
release: githubLink(`releases/tag/v${Package.version}`),
|
||||
license: githubLink("blob/main/LICENSE"),
|
||||
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations")
|
||||
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations"),
|
||||
privacy: githubLink("blob/main/PRIVACY.md")
|
||||
};
|
||||
|
||||
export const storeLink: string =
|
||||
|
||||
+72
-23
@@ -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> =>
|
||||
{
|
||||
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"),
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ export default defineConfig([
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/block-spacing": ["warn", "always"],
|
||||
"@stylistic/arrow-spacing": ["warn", { before: true, after: true }],
|
||||
"@stylistic/indent": ["warn", "tab"],
|
||||
"@stylistic/indent": ["warn", "tab", { assignmentOperator: "off" }],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/comma-spacing": ["warn"],
|
||||
"@stylistic/comma-dangle": ["warn", "never"],
|
||||
|
||||
@@ -1,3 +1,55 @@
|
||||
export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
|
||||
export { default as trackError } from "./utils/trackError";
|
||||
export { default as track } from "./utils/track";
|
||||
import { analytics } from "./utils/analytics";
|
||||
import analyticsPermission from "./utils/analyticsPermission";
|
||||
import { getUserProperties, userId } from "./utils/getUserProperties";
|
||||
|
||||
export { analyticsPermission };
|
||||
|
||||
export async function track(eventName: string, eventProperties?: Record<string, string>): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await analyticsPermission.getValue())
|
||||
return;
|
||||
|
||||
analytics.track(eventName, eventProperties);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send analytics event", ex);
|
||||
}
|
||||
}
|
||||
|
||||
export async function trackError(name: string, error: Error): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await analyticsPermission.getValue())
|
||||
return;
|
||||
|
||||
analytics.track(name, {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack ?? "no_stack"
|
||||
});
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send error report", ex);
|
||||
}
|
||||
}
|
||||
|
||||
export async function trackPage(pageName: string): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await analyticsPermission.getValue())
|
||||
return;
|
||||
|
||||
analytics.identify(await userId.getValue() as string, await getUserProperties());
|
||||
analytics.page(pageName);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send page view", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { createAnalytics } from "@wxt-dev/analytics";
|
||||
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
|
||||
|
||||
export const analytics = createAnalytics({
|
||||
providers:
|
||||
[
|
||||
googleAnalytics4({
|
||||
apiSecret: import.meta.env.WXT_GA4_API_SECRET,
|
||||
measurementId: import.meta.env.WXT_GA4_MEASUREMENT_ID
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||
import { analytics } from "./analytics";
|
||||
import { Permissions } from "wxt/browser";
|
||||
|
||||
const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
|
||||
{
|
||||
getValue: async (): Promise<boolean> =>
|
||||
{
|
||||
const isGranted: boolean = import.meta.env.FIREFOX
|
||||
? await browser.permissions.contains({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
})
|
||||
: await allowAnalytics.getValue();
|
||||
|
||||
analytics.setEnabled(isGranted);
|
||||
|
||||
return isGranted;
|
||||
},
|
||||
|
||||
setValue: async (value: boolean) =>
|
||||
{
|
||||
if (!import.meta.env.FIREFOX)
|
||||
{
|
||||
await allowAnalytics.setValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
let result: boolean = false;
|
||||
|
||||
if (value)
|
||||
result = await browser.permissions.request({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
});
|
||||
else
|
||||
result = await browser.permissions.remove({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
});
|
||||
|
||||
if (!result)
|
||||
throw new Error("Permission request was denied");
|
||||
},
|
||||
|
||||
watch: (cb: WatchCallback<boolean>): Unwatch =>
|
||||
{
|
||||
if (!import.meta.env.FIREFOX)
|
||||
return allowAnalytics.watch(cb);
|
||||
|
||||
const listener = async (permissions: Permissions.Permissions): Promise<void> =>
|
||||
{
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
if (permissions.data_collection?.includes("technicalAndInteraction"))
|
||||
{
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
const isGranted: boolean = await browser.permissions.contains({ data_collection: ["technicalAndInteraction"] });
|
||||
cb(isGranted, !isGranted);
|
||||
}
|
||||
};
|
||||
|
||||
browser.permissions.onAdded.addListener(listener);
|
||||
browser.permissions.onRemoved.addListener(listener);
|
||||
|
||||
return (): void =>
|
||||
{
|
||||
browser.permissions.onAdded.removeListener(listener);
|
||||
browser.permissions.onRemoved.removeListener(listener);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default analyticsPermission;
|
||||
|
||||
const allowAnalytics = storage.defineItem<boolean>("local:analytics", {
|
||||
fallback: true
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
|
||||
import { settings } from "@/utils/settings";
|
||||
|
||||
export async function getUserProperties(): Promise<UserProperties>
|
||||
{
|
||||
const properties: UserProperties =
|
||||
{
|
||||
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
|
||||
collection_count: (await collectionCount.getValue()).toString()
|
||||
};
|
||||
|
||||
for (const key of Object.keys(settings))
|
||||
{
|
||||
const value = await settings[key as keyof typeof settings].getValue();
|
||||
properties[`option_${key}`] = value.valueOf().toString();
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
export const userId = storage.defineItem("local:userId", {
|
||||
init: () => crypto.randomUUID()
|
||||
});
|
||||
|
||||
export type UserProperties =
|
||||
{
|
||||
collection_count: string;
|
||||
cloud_used: string;
|
||||
[key: `option_${string}`]: string;
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
export default function track(eventName: string, eventProperties?: Record<string, string>): void
|
||||
{
|
||||
try
|
||||
{
|
||||
analytics.track(eventName, eventProperties);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send analytics event", ex);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
export default function trackError(name: string, error: Error): void
|
||||
{
|
||||
try
|
||||
{
|
||||
analytics.track(name, {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack ?? "no_stack"
|
||||
});
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
console.error("Failed to send error report", ex);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
|
||||
import { settings } from "@/utils/settings";
|
||||
import { WxtStorageItem } from "wxt/storage";
|
||||
|
||||
// @ts-expect-error we don't need to implement a full storage item
|
||||
const userPropertiesStorage: WxtStorageItem<Record<string, string>, any> =
|
||||
{
|
||||
getValue: async (): Promise<UserProperties> =>
|
||||
{
|
||||
console.log("userPropertiesStorage.getValue");
|
||||
const properties: UserProperties =
|
||||
{
|
||||
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
|
||||
collection_count: (await collectionCount.getValue()).toString()
|
||||
};
|
||||
|
||||
for (const key of Object.keys(settings))
|
||||
{
|
||||
const value = await settings[key as keyof typeof settings].getValue();
|
||||
properties[`option_${key}`] = value.valueOf().toString();
|
||||
}
|
||||
|
||||
return properties;
|
||||
},
|
||||
setValue: async () => { }
|
||||
};
|
||||
|
||||
export default userPropertiesStorage;
|
||||
|
||||
export type UserProperties =
|
||||
{
|
||||
collection_count: string;
|
||||
cloud_used: string;
|
||||
[key: `option_${string}`]: string;
|
||||
};
|
||||
@@ -5,6 +5,9 @@ export { default as getCollections } from "./utils/getCollections";
|
||||
export { default as resoveConflict } from "./utils/resolveConflict";
|
||||
export { default as saveCollections } from "./utils/saveCollections";
|
||||
export { default as setCloudStorage } from "./utils/setCloudStorage";
|
||||
export { default as clearGraphicsStorage } from "./utils/clearGraphics";
|
||||
|
||||
export { default as thumbnailCaptureEnabled } from "./utils/thumbnailCaptureEnabled";
|
||||
|
||||
export const collectionCount = collectionStorage.count;
|
||||
export const graphics = collectionStorage.graphics;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
|
||||
export default async function clearGraphicsStorage(): Promise<void>
|
||||
{
|
||||
await collectionStorage.graphics.removeValue();
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Permissions } from "wxt/browser";
|
||||
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||
|
||||
const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
|
||||
{
|
||||
getValue: async (): Promise<boolean> =>
|
||||
await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] }),
|
||||
|
||||
watch: (cb: WatchCallback<boolean>): Unwatch =>
|
||||
{
|
||||
const listener = async (permissions: Permissions.Permissions): Promise<void> =>
|
||||
{
|
||||
if (permissions.permissions?.includes("scripting") || permissions.origins?.includes("<all_urls>"))
|
||||
{
|
||||
const isGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] });
|
||||
console.log("thumbnailCaptureEnabled changed", isGranted);
|
||||
cb(isGranted, !isGranted);
|
||||
}
|
||||
};
|
||||
|
||||
browser.permissions.onAdded.addListener(listener);
|
||||
browser.permissions.onRemoved.addListener(listener);
|
||||
|
||||
return (): void =>
|
||||
{
|
||||
browser.permissions.onAdded.removeListener(listener);
|
||||
browser.permissions.onRemoved.removeListener(listener);
|
||||
};
|
||||
},
|
||||
|
||||
setValue: async (value: boolean): Promise<void> =>
|
||||
{
|
||||
let result: boolean = false;
|
||||
|
||||
if (value)
|
||||
result = await browser.permissions.request({ permissions: ["scripting"], origins: ["<all_urls>"] });
|
||||
else
|
||||
{
|
||||
result = await browser.permissions.remove({ origins: ["<all_urls>"] });
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
await browser.permissions.request({ origins: ["http://localhost/*"] });
|
||||
}
|
||||
|
||||
if (!result)
|
||||
throw new Error("Permission request was denied");
|
||||
}
|
||||
};
|
||||
|
||||
export default thumbnailCaptureEnabled;
|
||||
@@ -0,0 +1,132 @@
|
||||
import { githubLinks } from "@/data/links";
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
import extLink from "@/utils/extLink";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||
import { reviewSettings } from "../utils/setSettingsReviewNeeded";
|
||||
import { Unwatch } from "wxt/storage";
|
||||
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
|
||||
export default function SettingsReviewDialog(): React.ReactElement
|
||||
{
|
||||
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
|
||||
const [captureThumbnails, setCaptureThumbnails] = useState<boolean | null>(null);
|
||||
const [needsReview, setNeedsReview] = useState<string[]>([]);
|
||||
const cls = useStyles();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
analyticsPermission.getValue().then(setAllowAnalytics);
|
||||
thumbnailCaptureEnabled.getValue().then(setCaptureThumbnails);
|
||||
settingsForReview.getValue().then(setNeedsReview);
|
||||
|
||||
const unwatchAnalytics: Unwatch = analyticsPermission.watch(setAllowAnalytics);
|
||||
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setCaptureThumbnails);
|
||||
|
||||
return () =>
|
||||
{
|
||||
unwatchAnalytics();
|
||||
unwatchThumbnails();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const updateAnalytics = (enabled: boolean): void =>
|
||||
{
|
||||
setAllowAnalytics(null);
|
||||
analyticsPermission.setValue(enabled)
|
||||
.catch(() => setAllowAnalytics(!enabled));
|
||||
};
|
||||
|
||||
const updateThumbnails = (enabled: boolean): void =>
|
||||
{
|
||||
setCaptureThumbnails(null);
|
||||
thumbnailCaptureEnabled.setValue(enabled)
|
||||
.catch(() => setCaptureThumbnails(!enabled));
|
||||
};
|
||||
|
||||
return (
|
||||
<fui.DialogSurface>
|
||||
<fui.DialogBody>
|
||||
<fui.DialogTitle>{ i18n.t("features.settingsReview.title") }</fui.DialogTitle>
|
||||
<fui.DialogContent className={ cls.content }>
|
||||
{ needsReview.includes(reviewSettings.THUMBNAILS) &&
|
||||
<div className={ cls.section }>
|
||||
<fui.Switch
|
||||
label={ i18n.t("options_page.storage.thumbnail_capture") }
|
||||
checked={ captureThumbnails ?? true }
|
||||
disabled={ captureThumbnails === null }
|
||||
onChange={ (_, e) => updateThumbnails(e.checked as boolean) } />
|
||||
|
||||
<fui.MessageBar layout="multiline">
|
||||
<fui.MessageBarBody className={ cls.msgBarBody }>
|
||||
<fui.MessageBarTitle>
|
||||
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }
|
||||
</fui.MessageBarTitle>
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
|
||||
</fui.Text>
|
||||
</fui.MessageBarBody>
|
||||
</fui.MessageBar>
|
||||
</div>
|
||||
}
|
||||
{ needsReview.includes(reviewSettings.ANALYTICS) &&
|
||||
<div className={ cls.section }>
|
||||
<fui.Switch
|
||||
label={ i18n.t("options_page.general.options.allow_analytics") }
|
||||
checked={ allowAnalytics ?? true }
|
||||
disabled={ allowAnalytics === null }
|
||||
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
|
||||
|
||||
<fui.MessageBar layout="multiline">
|
||||
<fui.MessageBarBody className={ cls.msgBarBody }>
|
||||
<fui.MessageBarTitle>
|
||||
{ i18n.t("features.settingsReview.analytics.title") }
|
||||
</fui.MessageBarTitle>
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("features.settingsReview.analytics.p1") }
|
||||
</fui.Text>
|
||||
<fui.Text as="p" weight="semibold">
|
||||
{ i18n.t("features.settingsReview.analytics.p2") }
|
||||
</fui.Text>
|
||||
<fui.Text as="p">
|
||||
{ i18n.t("features.settingsReview.analytics.p3_text") } <fui.Link { ...extLink(githubLinks.privacy) }>{ i18n.t("features.settingsReview.analytics.p3_link") }</fui.Link>.
|
||||
</fui.Text>
|
||||
</fui.MessageBarBody>
|
||||
</fui.MessageBar>
|
||||
</div>
|
||||
}
|
||||
</fui.DialogContent>
|
||||
<fui.DialogActions>
|
||||
<fui.Button onClick={ () => browser.runtime.openOptionsPage() }>
|
||||
{ i18n.t("features.settingsReview.action") }
|
||||
</fui.Button>
|
||||
<fui.DialogTrigger>
|
||||
<fui.Button appearance="primary">{ i18n.t("common.actions.save") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
</fui.DialogActions>
|
||||
</fui.DialogBody>
|
||||
</fui.DialogSurface>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
content:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalL
|
||||
},
|
||||
section:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalXS
|
||||
},
|
||||
msgBarBody:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
gap: fui.tokens.spacingVerticalXS,
|
||||
marginBottom: fui.tokens.spacingVerticalXS
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||
import SettingsReviewDialog from "../components/SettingsReviewDialog";
|
||||
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||
|
||||
export default function useSettingsReviewDialog(dialog: DialogContextType): Promise<void>
|
||||
{
|
||||
return new Promise<void>(res =>
|
||||
{
|
||||
settingsForReview.getValue().then(needsReview =>
|
||||
{
|
||||
if (needsReview.length > 0)
|
||||
dialog.pushCustom(
|
||||
<SettingsReviewDialog />,
|
||||
undefined,
|
||||
() =>
|
||||
{
|
||||
settingsForReview.removeValue();
|
||||
res();
|
||||
}
|
||||
);
|
||||
else
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useSettingsReviewDialog } from "./hooks/useSettingsReviewDialog";
|
||||
@@ -0,0 +1 @@
|
||||
export { default as setSettingsReviewNeeded } from "./setSettingsReviewNeeded";
|
||||
@@ -0,0 +1,66 @@
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
import { Runtime } from "wxt/browser";
|
||||
import { settingsForReview } from "./showSettingsReviewDialog";
|
||||
|
||||
export default async function setSettingsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<void>
|
||||
{
|
||||
const needsReview: string[] = await settingsForReview.getValue();
|
||||
|
||||
if (!needsReview.includes(reviewSettings.ANALYTICS) && await checkAnalyticsReviewNeeded(installReason, previousVersion))
|
||||
needsReview.push(reviewSettings.ANALYTICS);
|
||||
|
||||
if (!needsReview.includes(reviewSettings.THUMBNAILS) && await checkThumbnailsReviewNeeded(installReason, previousVersion))
|
||||
needsReview.push(reviewSettings.THUMBNAILS);
|
||||
|
||||
console.log("Settings needing review:", needsReview);
|
||||
// Add more settings here as needed
|
||||
|
||||
if (needsReview.length > 0)
|
||||
await settingsForReview.setValue(needsReview);
|
||||
}
|
||||
|
||||
export const reviewSettings =
|
||||
{
|
||||
ANALYTICS: "analytics",
|
||||
THUMBNAILS: "thumbnails"
|
||||
};
|
||||
|
||||
async function checkAnalyticsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<boolean>
|
||||
{
|
||||
if (installReason === "install")
|
||||
return !await analyticsPermission.getValue();
|
||||
|
||||
if (installReason === "update")
|
||||
{
|
||||
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
|
||||
const cumulative: number = major * 10000 + minor * 100 + patch;
|
||||
|
||||
if (cumulative < 30100) // < 3.1.0
|
||||
return true;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkThumbnailsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<boolean>
|
||||
{
|
||||
if (installReason === "install")
|
||||
return true;
|
||||
|
||||
if (installReason === "update")
|
||||
{
|
||||
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
|
||||
const cumulative: number = major * 10000 + minor * 100 + patch;
|
||||
|
||||
if (cumulative < 30100) // < 3.1.0
|
||||
return true;
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const settingsForReview = storage.defineItem<string[]>(
|
||||
"local:settingsForReview",
|
||||
{
|
||||
fallback: []
|
||||
}
|
||||
);
|
||||
@@ -1,17 +1,25 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||
import WelcomeDialog from "../components/WelcomeDialog";
|
||||
import { showWelcomeDialog } from "../utils/showWelcomeDialog";
|
||||
|
||||
export default function useWelcomeDialog(): void
|
||||
export default function useWelcomeDialog(dialog: DialogContextType): Promise<void>
|
||||
{
|
||||
const dialog = useDialog();
|
||||
|
||||
useEffect(() =>
|
||||
return new Promise<void>(res =>
|
||||
{
|
||||
showWelcomeDialog.getValue().then(showWelcome =>
|
||||
{
|
||||
if (showWelcome || import.meta.env.DEV)
|
||||
dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
|
||||
dialog.pushCustom(
|
||||
<WelcomeDialog />,
|
||||
undefined,
|
||||
() =>
|
||||
{
|
||||
showWelcomeDialog.removeValue();
|
||||
res();
|
||||
}
|
||||
);
|
||||
else
|
||||
res();
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { DialogContextType, useDialog } from "@/contexts/DialogProvider";
|
||||
|
||||
export default function useDialogTrain(...dialogs: ((dialog: DialogContextType) => Promise<void>)[]): void
|
||||
{
|
||||
const dialog = useDialog();
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
(async () =>
|
||||
{
|
||||
for (const item of dialogs)
|
||||
{
|
||||
await item(dialog);
|
||||
await new Promise(res => setTimeout(res, 250));
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "Visit our dev blog to learn more about this update and all of its features!"
|
||||
actions:
|
||||
visit_blog: "Read dev blog"
|
||||
settingsReview:
|
||||
title: "Review your settings"
|
||||
action: "All settings"
|
||||
analytics:
|
||||
title: "These statistics will help us improve the extension"
|
||||
p1: "We only collect usage statistics (number of collections, used features, etc.)"
|
||||
p2: "We do not collect any of your data!"
|
||||
p3_text: "See the full list of what we collect"
|
||||
p3_link: "here"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Show counter badge"
|
||||
show_notification: "Show notification when saving tabs using context menu"
|
||||
unload_tabs: "Do not load tabs after opening"
|
||||
allow_analytics: "Allow collection of anonymous statistics"
|
||||
list_locations:
|
||||
title: "Open collection list in:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized."
|
||||
action: "Disable and reload the extension"
|
||||
thumbnail_capture: "Capture thumbnails and icons for saved tabs"
|
||||
thumbnail_capture_notice1: "Requires permission to access content on visited websites"
|
||||
thumbnail_capture_notice2: "Disabling this feature may improve performance on large collections"
|
||||
clear_thumbnails:
|
||||
action: "Clear saved thumbnails"
|
||||
title: "Delete all saved thumbnails?"
|
||||
prompt: "This action will remove all saved thumbnails, previews and icons for your saved tabs. This action cannot be undone."
|
||||
about:
|
||||
title: "About"
|
||||
developed_by: "Developed by Eugene Fox"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "My website"
|
||||
source: "Source code"
|
||||
changelog: "Changelog"
|
||||
privacy: "Privacy policy"
|
||||
|
||||
collections:
|
||||
empty: "This collection is empty"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!"
|
||||
actions:
|
||||
visit_blog: "Leer el blog de desarrollo"
|
||||
settingsReview:
|
||||
title: "Revisa tus ajustes"
|
||||
action: "Todos los ajustes"
|
||||
analytics:
|
||||
title: "Estas estadísticas nos ayudarán a mejorar la extensión"
|
||||
p1: "Solo recopilamos estadísticas de uso (número de colecciones, funciones utilizadas, etc.)"
|
||||
p2: "¡No recopilamos ninguno de tus datos!"
|
||||
p3_text: "Ver la lista completa de lo que recopilamos"
|
||||
p3_link: "aquí"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Mostrar insignia de contador"
|
||||
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
|
||||
unload_tabs: "No cargar pestañas después de abrir"
|
||||
allow_analytics: "Permitir la recopilación de estadísticas anónimas"
|
||||
list_locations:
|
||||
title: "Abrir lista de colecciones en:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Esta acción deshabilitará la sincronización de colecciones entre tus dispositivos. La configuración de la extensión seguirá sincronizándose."
|
||||
action: "Deshabilitar y recargar la extensión"
|
||||
thumbnail_capture: "Capturar miniaturas e íconos para las pestañas guardadas"
|
||||
thumbnail_capture_notice1: "Requiere permiso para acceder al contenido de los sitios web visitados"
|
||||
thumbnail_capture_notice2: "Deshabilitar esta función puede mejorar el rendimiento en colecciones grandes"
|
||||
clear_thumbnails:
|
||||
action: "Eliminar miniaturas guardadas"
|
||||
title: "¿Eliminar todas las miniaturas guardadas?"
|
||||
prompt: "Esta acción eliminará todas las miniaturas, vistas previas e íconos guardados para tus pestañas guardadas. Esta acción no se puede deshacer."
|
||||
about:
|
||||
title: "Acerca de"
|
||||
developed_by: "Desarrollado por Eugene Fox"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "Mi sitio web"
|
||||
source: "Código fuente"
|
||||
changelog: "Registro de cambios"
|
||||
privacy: "Política de privacidad"
|
||||
|
||||
collections:
|
||||
empty: "Esta colección está vacía"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!"
|
||||
actions:
|
||||
visit_blog: "Leggi il blog degli sviluppatori"
|
||||
settingsReview:
|
||||
title: "Rivedi le tue impostazioni"
|
||||
action: "Tutte le impostazioni"
|
||||
analytics:
|
||||
title: "Queste statistiche ci aiuteranno a migliorare l'estensione"
|
||||
p1: "Raccogliamo solo statistiche di utilizzo (numero di collezioni, funzionalità utilizzate, ecc.)"
|
||||
p2: "Non raccogliamo nessuno dei tuoi dati!"
|
||||
p3_text: "Vedi l'elenco completo di ciò che raccogliamo"
|
||||
p3_link: "qui"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Mostra il badge del contatore"
|
||||
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
|
||||
unload_tabs: "Non caricare le schede dopo l'apertura"
|
||||
allow_analytics: "Consenti la raccolta di statistiche anonime"
|
||||
list_locations:
|
||||
title: "Apri elenco delle collezioni in:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Questa azione disabiliterà la sincronizzazione delle collezioni tra i tuoi dispositivi. Le impostazioni dell'estensione saranno comunque sincronizzate."
|
||||
action: "Disabilita e ricarica l'estensione"
|
||||
thumbnail_capture: "Cattura miniature e icone per le schede salvate"
|
||||
thumbnail_capture_notice1: "Richiede il permesso di accedere ai contenuti dei siti web visitati"
|
||||
thumbnail_capture_notice2: "Disabilitare questa funzione può migliorare le prestazioni su collezioni di grandi dimensioni"
|
||||
clear_thumbnails:
|
||||
action: "Elimina miniature salvate"
|
||||
title: "Eliminare tutte le miniature salvate?"
|
||||
prompt: "Questa azione rimuoverà tutte le miniature, anteprime e icone salvate per le tue schede salvate. Questa azione non può essere annullata."
|
||||
about:
|
||||
title: "Informazioni"
|
||||
developed_by: "Sviluppato da Eugene Fox"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "Il mio sito web"
|
||||
source: "Codice sorgente"
|
||||
changelog: "Registro delle modifiche"
|
||||
privacy: "Politica sulla riservatezza"
|
||||
|
||||
collections:
|
||||
empty: "Questa collezione è vuota"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "Odwiedź blog dewelopera (tylko w języku angielskim), aby dowiedzieć się więcej o tej aktualizacji i wszystkich jej funkcjach!"
|
||||
actions:
|
||||
visit_blog: "Czytaj blog"
|
||||
settingsReview:
|
||||
title: "Sprawdź ustawienia"
|
||||
action: "Wszystkie ustawienia"
|
||||
analytics:
|
||||
title: "Ta statystyka pozwoli ulepszać rozszerzenie"
|
||||
p1: "Zbieramy tylko statystyki użycia (liczba kolekcji, używane funkcje itp.)"
|
||||
p2: "Nie zbieramy twoich danych osobowych!"
|
||||
p3_text: "Pełną listę zbieranych danych można zobaczyć"
|
||||
p3_link: "tutaj"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Pokaż licznik"
|
||||
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
|
||||
unload_tabs: "Nie ładuj kart po otwarciu"
|
||||
allow_analytics: "Zezwól na zbieranie anonimowej statystyki"
|
||||
list_locations:
|
||||
title: "Otwieraj listę kolekcji w:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Ta akcja wyłączy synchronizację kolekcji między twoimi urządzeniami. Ustawienia nadal będą przechowywane w chmurze."
|
||||
action: "Wyłącz i przeładuj rozszerzenie"
|
||||
thumbnail_capture: "Zapisuj podglądy i ikony dla zapisanych kart"
|
||||
thumbnail_capture_notice1: "Wymagany dostęp do zawartości odwiedzanych stron internetowych"
|
||||
thumbnail_capture_notice2: "Wyłączenie tej funkcji może poprawić wydajność przy dużej liczbie zapisanych kart"
|
||||
clear_thumbnails:
|
||||
action: "Usuń zapisane ikony"
|
||||
title: "Usunąć podglądy i ikony?"
|
||||
prompt: "Ta akcja usunie wszystkie podglądy i ikony twoich zapisanych kart. Tej akcji nie można cofnąć."
|
||||
about:
|
||||
title: "O rozszerzeniu"
|
||||
developed_by: "Wywoływacz: Eugeniusz Lis"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "Moja strona internetowa"
|
||||
source: "Kod źródłowy"
|
||||
changelog: "Lista zmian"
|
||||
privacy: "Polityka prywatności"
|
||||
|
||||
collections:
|
||||
empty: "Ta kolekcja jest pusta"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!"
|
||||
actions:
|
||||
visit_blog: "Ler blog de desenvolvimento"
|
||||
settingsReview:
|
||||
title: "Revise suas configurações"
|
||||
action: "Todas as configurações"
|
||||
analytics:
|
||||
title: "Estas estatísticas nos ajudarão a melhorar a extensão"
|
||||
p1: "Nós coletamos apenas estatísticas de uso (número de coleções, recursos usados, etc.)"
|
||||
p2: "Nós não coletamos nenhum dos seus dados!"
|
||||
p3_text: "Veja a lista completa do que coletamos"
|
||||
p3_link: "aqui"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Mostrar contador no ícone"
|
||||
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
|
||||
unload_tabs: "Não carregar abas após abrir"
|
||||
allow_analytics: "Permitir coleta de estatísticas anônimas"
|
||||
list_locations:
|
||||
title: "Abrir lista de coleções em:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Esta ação desativará a sincronização de coleções entre seus dispositivos. As configurações da extensão ainda serão sincronizadas."
|
||||
action: "Desativar e recarregar a extensão"
|
||||
thumbnail_capture: "Capturar miniaturas e ícones para as abas salvas"
|
||||
thumbnail_capture_notice1: "Requer permissão para acessar o conteúdo dos sites visitados"
|
||||
thumbnail_capture_notice2: "Desativar esse recurso pode melhorar o desempenho em coleções grandes"
|
||||
clear_thumbnails:
|
||||
action: "Eliminar miniaturas guardadas"
|
||||
title: "Excluir todas as miniaturas salvas?"
|
||||
prompt: "Esta ação removerá todas as miniaturas, pré-visualizações e ícones salvos para suas abas salvas. Esta ação não pode ser desfeita."
|
||||
about:
|
||||
title: "Sobre"
|
||||
developed_by: "Desenvolvido por Eugene Fox"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "Meu site"
|
||||
source: "Código-fonte"
|
||||
changelog: "Registro de alterações"
|
||||
privacy: "Política de Privacidade"
|
||||
|
||||
collections:
|
||||
empty: "Esta coleção está vazia"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!"
|
||||
actions:
|
||||
visit_blog: "Читать блог"
|
||||
settingsReview:
|
||||
title: "Проверьте настройки"
|
||||
action: "Все настройки"
|
||||
analytics:
|
||||
title: "Эта статистика позволит улучшать расширение"
|
||||
p1: "Мы собираем только статистику использования (количество коллекций, используемые функции и т.д.)"
|
||||
p2: "Мы не собираем ваши личные данные!"
|
||||
p3_text: "Полный список собираемых данных можно посмотреть"
|
||||
p3_link: "здесь"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Показывать счетчик"
|
||||
show_notification: "Показывать уведомление при сохранении через контекстное меню"
|
||||
unload_tabs: "Не загружать вкладки после открытия"
|
||||
allow_analytics: "Разрешить сбор анонимной статистики"
|
||||
list_locations:
|
||||
title: "Открывать список коллекций в:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке."
|
||||
action: "Отключить и перезагрузить расширение"
|
||||
thumbnail_capture: "Сохранять превью и иконки для сохранённых вкладок"
|
||||
thumbnail_capture_notice1: "Необходим доступ к содержанию посещенных веб-сайтов"
|
||||
thumbnail_capture_notice2: "Отключение этой функции может улучшить производительность при большом количестве сохраненных вкладок"
|
||||
clear_thumbnails:
|
||||
action: "Удалить сохранённые иконки"
|
||||
title: "Удалить превью и иконки?"
|
||||
prompt: "Это действие удалит все превью и иконки у ваших сохраненных вкладок. Это действие не может быть отменено."
|
||||
about:
|
||||
title: "О расширении"
|
||||
developed_by: "Разработчик: Евгений Лис"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "Мой веб-сайт"
|
||||
source: "Исходный код"
|
||||
changelog: "Список изменений"
|
||||
privacy: "Политика конфиденциальности"
|
||||
|
||||
collections:
|
||||
empty: "Эта коллекция пуста"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!"
|
||||
actions:
|
||||
visit_blog: "Читати блог"
|
||||
settingsReview:
|
||||
title: "Перевірте налаштування"
|
||||
action: "Всi налаштування"
|
||||
analytics:
|
||||
title: "Ця статистика дозволить покращувати розширення"
|
||||
p1: "Ми збираємо лише статистику використання (кількість колекцій, використовувані функції тощо)"
|
||||
p2: "Ми не збираємо ваші особисті дані!"
|
||||
p3_text: "Повний список зібраних даних можна подивитися"
|
||||
p3_link: "тут"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "Показувати лічильник"
|
||||
show_notification: "Показувати сповіщення при збереженні через контекстне меню"
|
||||
unload_tabs: "Не завантажувати вкладки після відкриття"
|
||||
allow_analytics: "Дозволити збір анонімної статистики"
|
||||
list_locations:
|
||||
title: "Відкривати список колекцій у:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
|
||||
action: "Вимкнути та перезавантажити розширення"
|
||||
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок"
|
||||
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
|
||||
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
|
||||
clear_thumbnails:
|
||||
action: "Видалити збережені іконки"
|
||||
title: "Видалити превью і іконки?"
|
||||
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
|
||||
about:
|
||||
title: "О розширенні"
|
||||
developed_by: "Розробник: Євген Лис"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "Мій веб-сайт"
|
||||
source: "Вихідний код"
|
||||
changelog: "Список змін"
|
||||
privacy: "Політика конфіденційності"
|
||||
|
||||
collections:
|
||||
empty: "Ця колекція пуста"
|
||||
|
||||
@@ -36,6 +36,15 @@ features:
|
||||
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
|
||||
actions:
|
||||
visit_blog: "阅读开发博客"
|
||||
settingsReview:
|
||||
title: "检查您的设置"
|
||||
action: "所有设置"
|
||||
analytics:
|
||||
title: "这些统计数据将帮助我们改进扩展"
|
||||
p1: "我们只收集使用统计数据(收藏数量、使用的功能等)"
|
||||
p2: "我们不会收集您的任何数据!"
|
||||
p3_text: ""
|
||||
p3_link: "请参阅我们收集内容的完整列表"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -74,6 +83,7 @@ options_page:
|
||||
show_badge: "显示计数徽章"
|
||||
show_notification: "使用上下文菜单保存标签时显示通知"
|
||||
unload_tabs: "打开后不加载标签"
|
||||
allow_analytics: "允许收集匿名统计数据"
|
||||
list_locations:
|
||||
title: "在以下位置打开收藏列表:"
|
||||
options:
|
||||
@@ -121,6 +131,13 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
|
||||
action: "禁用并重新加载扩展"
|
||||
thumbnail_capture: "为已保存的标签保存缩略图和图标"
|
||||
thumbnail_capture_notice1: "需要访问已访问网站内容的权限"
|
||||
thumbnail_capture_notice2: "当你有大量集合时,禁用此功能可能会提高性能"
|
||||
clear_thumbnails:
|
||||
action: "删除已保存的图标"
|
||||
title: "删除缩略图和图标?"
|
||||
prompt: "此操作将删除您已保存标签页的所有缩略图和图标。此操作无法撤消。"
|
||||
about:
|
||||
title: "关于"
|
||||
developed_by: "由尤金·福克斯开发"
|
||||
@@ -133,6 +150,7 @@ options_page:
|
||||
website: "我的网站"
|
||||
source: "源代码"
|
||||
changelog: "更新日志"
|
||||
privacy: "隐私政策"
|
||||
|
||||
collections:
|
||||
empty: "此收藏为空"
|
||||
|
||||
+16
-17
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tabs-aside",
|
||||
"private": true,
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
@@ -16,31 +16,30 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fluentui/react-components": "^9.68.1",
|
||||
"@fluentui/react-icons": "^2.0.307",
|
||||
"@fluentui/react-components": "^9.70.0",
|
||||
"@fluentui/react-icons": "^2.0.309",
|
||||
"@webext-core/messaging": "^2.3.0",
|
||||
"@wxt-dev/analytics": "^0.4.1",
|
||||
"@wxt-dev/i18n": "^0.2.4",
|
||||
"lzutf8": "^0.6.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react": "~18.3.1",
|
||||
"react-dom": "~18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "^0.10.0",
|
||||
"@eslint/js": "^9.32.0",
|
||||
"@eslint/json": "^0.13.1",
|
||||
"@stylistic/eslint-plugin": "^5.2.2",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/scheduler": "0.23.0",
|
||||
"@wxt-dev/module-react": "^1.1.3",
|
||||
"eslint": "^9.32.0",
|
||||
"@eslint/css": "^0.11.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@eslint/json": "^0.13.2",
|
||||
"@stylistic/eslint-plugin": "^5.3.1",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "~18.3.1",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"scheduler": "0.23.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^7.0.6",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"vite": "^7.1.5",
|
||||
"wxt": "~0.19.29"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
|
||||
+15
-4
@@ -2,7 +2,7 @@ import { ConfigEnv, defineConfig, UserManifest } from "wxt";
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module", "@wxt-dev/analytics/module"],
|
||||
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module"],
|
||||
vite: () => ({
|
||||
build:
|
||||
{
|
||||
@@ -37,10 +37,15 @@ export default defineConfig({
|
||||
"tabs",
|
||||
"notifications",
|
||||
"contextMenus",
|
||||
"bookmarks",
|
||||
"tabGroups"
|
||||
],
|
||||
|
||||
optional_permissions:
|
||||
[
|
||||
"bookmarks",
|
||||
"scripting"
|
||||
],
|
||||
|
||||
commands:
|
||||
{
|
||||
show_collections:
|
||||
@@ -71,7 +76,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
|
||||
host_permissions: ["<all_urls>"]
|
||||
optional_host_permissions: ["<all_urls>"]
|
||||
};
|
||||
|
||||
if (browser === "firefox")
|
||||
@@ -80,7 +85,13 @@ export default defineConfig({
|
||||
gecko:
|
||||
{
|
||||
id: "tabsaside@xfox111.net",
|
||||
strict_min_version: "139.0"
|
||||
strict_min_version: "139.0",
|
||||
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection_permissions: {
|
||||
required: ["browsingActivity"],
|
||||
optional: ["technicalAndInteraction"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user