mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-07-02 19:52:47 +03:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2065ee4637 | |||
| b51dd6083f | |||
| 3cd3c4453d | |||
| 816c8bf28c | |||
| c27c9f7b33 | |||
| def52278ff | |||
| 101a72e6e3 |
+27
-1
@@ -16,7 +16,33 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
open-pull-requests-limit: 20
|
||||
groups:
|
||||
deps:
|
||||
patterns:
|
||||
- "*"
|
||||
exclude-patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
- "scheduler"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
react-next:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
update-types:
|
||||
- major
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
@@ -75,10 +75,11 @@ 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
|
||||
continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }}
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,4 +83,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
@@ -37,7 +37,7 @@ Check out our [latest blog post](https://at.xfox111.net/tabs-aside-3-0) regardin
|
||||
|
||||
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin)
|
||||
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd)
|
||||
- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/)
|
||||
- [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/ms-edge-tabs-aside/)
|
||||
- [GitHub Releases](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||
|
||||
### Sideloading (for testing purposes only)
|
||||
|
||||
+1
-1
@@ -20,6 +20,6 @@ export const githubLinks =
|
||||
export const storeLink: string =
|
||||
import.meta.env.FIREFOX
|
||||
? "https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/" :
|
||||
chrome.runtime.getManifest().update_url?.startsWith("https://edge.microsoft.com/") ?
|
||||
browser.runtime.getManifest().update_url?.startsWith("https://edge.microsoft.com/") ?
|
||||
"https://microsoftedge.microsoft.com/addons/detail/tabs-aside/kmnblllmalkiapkfknnlpobmjjdnlhnd" :
|
||||
"https://chromewebstore.google.com/detail/tabs-aside/mgmjbodjgijnebfgohlnjkegdpbdjgin";
|
||||
|
||||
+74
-26
@@ -1,20 +1,25 @@
|
||||
import { track, trackError } from "@/features/analytics";
|
||||
import { collectionCount, getCollections, thumbnailCaptureEnabled, saveCollections } from "@/features/collectionStorage";
|
||||
import { collectionCount, getCollections, saveCollections, thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
import { collectionStorage } from "@/features/collectionStorage/utils/collectionStorage";
|
||||
import getCollectionsFromCloud from "@/features/collectionStorage/utils/getCollectionsFromCloud";
|
||||
import getCollectionsFromLocal from "@/features/collectionStorage/utils/getCollectionsFromLocal";
|
||||
import { migrateStorage } from "@/features/migration";
|
||||
import { setSettingsReviewNeeded } from "@/features/settingsReview/utils";
|
||||
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
|
||||
import { SettingsValue } from "@/hooks/useSettings";
|
||||
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
import { closeTabsAsync } from "@/utils/closeTabsAsync";
|
||||
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import { onMessage, sendMessage } from "@/utils/messaging";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import { settings } from "@/utils/settings";
|
||||
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";
|
||||
import { Unwatch } from "wxt/utils/storage";
|
||||
import { openCollection, openGroup } from "./sidepanel/utils/opener";
|
||||
|
||||
export default defineBackground(() =>
|
||||
{
|
||||
@@ -36,16 +41,42 @@ export default defineBackground(() =>
|
||||
logger("onInstalled", reason, previousVersion);
|
||||
track("extension_installed", { reason, previousVersion: previousVersion ?? "none" });
|
||||
|
||||
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
|
||||
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
|
||||
const cumulative: number = major * 10000 + minor * 100 + patch;
|
||||
|
||||
await setSettingsReviewNeeded(reason, previousVersion);
|
||||
|
||||
if (reason === "update" && previousMajor < 3)
|
||||
if (reason === "update" && cumulative < 30000) // < 3.0.0
|
||||
{
|
||||
await migrateStorage();
|
||||
await showWelcomeDialog.setValue(true);
|
||||
browser.runtime.reload();
|
||||
}
|
||||
|
||||
if (reason === "update" && cumulative >= 30000 && cumulative < 30200) // >= 3.0.0 && < 3.2.0
|
||||
{
|
||||
// Merge cloud and local storage if they are out of sync
|
||||
const localTimestamp: number = await collectionStorage.localLastUpdated.getValue();
|
||||
const syncTimestamp: number = await collectionStorage.syncLastUpdated.getValue();
|
||||
|
||||
if (localTimestamp === syncTimestamp)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
const localCollections: CollectionItem[] = await getCollectionsFromLocal();
|
||||
const cloudCollections: CollectionItem[] = await getCollectionsFromCloud();
|
||||
const mergedCollections: CollectionItem[] = [...cloudCollections, ...localCollections];
|
||||
|
||||
await saveCollections(mergedCollections, true, graphicsCache);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to merge cloud and local storage during update");
|
||||
trackError("cloud_sync_merge_error", ex as Error);
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
browser.commands.onCommand.addListener(
|
||||
@@ -67,7 +98,7 @@ export default defineBackground(() =>
|
||||
let unwatchAddThumbnail: RemoveListenerCallback | null = null;
|
||||
let captureInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const captureFavicon = (_: any, __: any, tab: Tabs.Tab): void =>
|
||||
const captureFavicon = (_: any, __: any, tab: Browser.tabs.Tab): void =>
|
||||
{
|
||||
if (!tab.url)
|
||||
return;
|
||||
@@ -79,7 +110,7 @@ export default defineBackground(() =>
|
||||
};
|
||||
};
|
||||
|
||||
const tryCaptureTab = async (tab: Tabs.Tab): Promise<void> =>
|
||||
const tryCaptureTab = async (tab: Browser.tabs.Tab): Promise<void> =>
|
||||
{
|
||||
if (!tab.url || tab.status !== "complete" || !tab.active)
|
||||
return;
|
||||
@@ -91,7 +122,7 @@ export default defineBackground(() =>
|
||||
{
|
||||
// We use chrome here because polyfill throws uncatchable errors for some reason
|
||||
// It's a compatible API anyway
|
||||
const capture: string = await chrome.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
|
||||
const capture: string = await browser.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
|
||||
|
||||
if (capture)
|
||||
{
|
||||
@@ -255,6 +286,7 @@ export default defineBackground(() =>
|
||||
};
|
||||
|
||||
const toggleSidebarFirefox = async (): Promise<void> =>
|
||||
// @ts-expect-error Firefox-only API
|
||||
await browser.sidebarAction.toggle();
|
||||
|
||||
const updateButton = async (action: SettingsValue<"contextAction">): Promise<void> =>
|
||||
@@ -271,7 +303,7 @@ export default defineBackground(() =>
|
||||
unwatchActionTitle?.();
|
||||
|
||||
if (!import.meta.env.FIREFOX)
|
||||
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
|
||||
await browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
|
||||
|
||||
// Setup new behavior
|
||||
if (action === "action")
|
||||
@@ -290,7 +322,7 @@ export default defineBackground(() =>
|
||||
if (import.meta.env.FIREFOX)
|
||||
browser.action.onClicked.addListener(toggleSidebarFirefox);
|
||||
else
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
}
|
||||
else if (location !== "popup")
|
||||
browser.action.onClicked.addListener(openCollectionsInTab);
|
||||
@@ -309,17 +341,17 @@ export default defineBackground(() =>
|
||||
{
|
||||
logger("enforcePinnedTab");
|
||||
|
||||
const openWindows: Windows.Window[] = await browser.windows.getAll({ populate: true });
|
||||
const openWindows: Browser.windows.Window[] = await browser.windows.getAll({ populate: true });
|
||||
|
||||
for (const openWindow of openWindows)
|
||||
{
|
||||
if (openWindow.incognito || openWindow.type !== "normal")
|
||||
continue;
|
||||
|
||||
const activeTabs: Tabs.Tab[] = openWindow.tabs!.filter(tab =>
|
||||
const activeTabs: Browser.tabs.Tab[] = openWindow.tabs!.filter(tab =>
|
||||
tab.url === browser.runtime.getURL("/sidepanel.html"));
|
||||
|
||||
const targetTab: Tabs.Tab | undefined = activeTabs.find(tab => tab.pinned);
|
||||
const targetTab: Browser.tabs.Tab | undefined = activeTabs.find(tab => tab.pinned);
|
||||
|
||||
if (!targetTab)
|
||||
await browser.tabs.create({
|
||||
@@ -329,7 +361,7 @@ export default defineBackground(() =>
|
||||
pinned: true
|
||||
});
|
||||
|
||||
const tabsToClose: Tabs.Tab[] = activeTabs.filter(tab => tab.id !== targetTab?.id);
|
||||
const tabsToClose: Browser.tabs.Tab[] = activeTabs.filter(tab => tab.id !== targetTab?.id);
|
||||
|
||||
if (tabsToClose.length > 0)
|
||||
await browser.tabs.remove(tabsToClose.map(tab => tab.id!));
|
||||
@@ -341,7 +373,7 @@ export default defineBackground(() =>
|
||||
logger("updateView", viewLocation);
|
||||
|
||||
browser.tabs.onHighlighted.removeListener(enforcePinnedTab);
|
||||
const tabs: Tabs.Tab[] = await browser.tabs.query({
|
||||
const tabs: Browser.tabs.Tab[] = await browser.tabs.query({
|
||||
url: browser.runtime.getURL("/sidepanel.html")
|
||||
});
|
||||
await browser.tabs.remove(tabs.map(tab => tab.id!));
|
||||
@@ -351,11 +383,12 @@ export default defineBackground(() =>
|
||||
});
|
||||
|
||||
if (import.meta.env.FIREFOX)
|
||||
// @ts-expect-error Firefox-only API
|
||||
await browser.sidebarAction.setPanel({
|
||||
panel: viewLocation === "sidebar" ? browser.runtime.getURL("/sidepanel.html") : ""
|
||||
});
|
||||
else
|
||||
await chrome.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
|
||||
await browser.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
|
||||
|
||||
if (viewLocation === "pinned")
|
||||
{
|
||||
@@ -386,9 +419,10 @@ export default defineBackground(() =>
|
||||
if (view === "sidebar")
|
||||
{
|
||||
if (import.meta.env.FIREFOX)
|
||||
// @ts-expect-error Firefox-only API
|
||||
browser.sidebarAction.open();
|
||||
else
|
||||
chrome.sidePanel.open({ windowId });
|
||||
browser.sidePanel.open({ windowId });
|
||||
}
|
||||
else
|
||||
browser.action.openPopup();
|
||||
@@ -398,11 +432,11 @@ export default defineBackground(() =>
|
||||
{
|
||||
logger("openCollectionsInTab");
|
||||
|
||||
const currentWindow: Windows.Window = await browser.windows.getCurrent({ populate: true });
|
||||
const currentWindow: Browser.windows.Window = await browser.windows.getCurrent({ populate: true });
|
||||
|
||||
if (currentWindow.incognito)
|
||||
{
|
||||
let availableWindows: Windows.Window[] = await browser.windows.getAll({ populate: true });
|
||||
let availableWindows: Browser.windows.Window[] = await browser.windows.getAll({ populate: true });
|
||||
|
||||
availableWindows = availableWindows.filter(window =>
|
||||
!window.incognito &&
|
||||
@@ -411,7 +445,7 @@ export default defineBackground(() =>
|
||||
|
||||
if (availableWindows.length > 0)
|
||||
{
|
||||
const availableTab: Tabs.Tab = availableWindows[0].tabs!.find(
|
||||
const availableTab: Browser.tabs.Tab = availableWindows[0].tabs!.find(
|
||||
tab => tab.url === browser.runtime.getURL("/sidepanel.html")
|
||||
)!;
|
||||
|
||||
@@ -428,7 +462,7 @@ export default defineBackground(() =>
|
||||
}
|
||||
else
|
||||
{
|
||||
const collectionTab: Tabs.Tab | undefined = currentWindow.tabs!.find(
|
||||
const collectionTab: Browser.tabs.Tab | undefined = currentWindow.tabs!.find(
|
||||
tab => tab.url === browser.runtime.getURL("/sidepanel.html")
|
||||
);
|
||||
|
||||
@@ -447,14 +481,28 @@ export default defineBackground(() =>
|
||||
{
|
||||
logger("saveTabs", closeAfterSave);
|
||||
|
||||
const collection: CollectionItem = await saveTabsToCollection(closeAfterSave);
|
||||
const [tabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (tabs.length < 1)
|
||||
{
|
||||
await sendPartialSaveNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
const collection: CollectionItem = await createCollectionFromTabs(tabs);
|
||||
const [savedCollections, cloudIssue] = await getCollections();
|
||||
const newList = [collection, ...savedCollections];
|
||||
|
||||
await saveCollections(newList, cloudIssue === null, graphicsCache);
|
||||
|
||||
track(closeAfterSave ? "set_aside" : "save");
|
||||
sendMessage("refreshCollections", undefined);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
|
||||
if (closeAfterSave)
|
||||
await closeTabsAsync(tabs);
|
||||
|
||||
if (await settings.notifyOnSave.getValue())
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.tabs_saved.title"),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
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,6 +14,7 @@ export default function GeneralSection(): React.ReactElement
|
||||
const [dismissOnLoad, setDismissOnLoad] = useSettings("dismissOnLoad");
|
||||
const [listLocation, setListLocation] = useSettings("listLocation");
|
||||
const [contextAction, setContextAction] = useSettings("contextAction");
|
||||
const [showPartialSaveNotification, setShowPartialSaveNotification] = useSettings("showPartialSaveNotification");
|
||||
|
||||
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
|
||||
|
||||
@@ -44,6 +45,7 @@ export default function GeneralSection(): React.ReactElement
|
||||
setContextAction("open");
|
||||
|
||||
if (import.meta.env.FIREFOX && e.optionValue !== "sidebar")
|
||||
// @ts-expect-error Firefox-only API
|
||||
browser.sidebarAction.close();
|
||||
|
||||
setListLocation(e.optionValue as ListLocationType);
|
||||
@@ -72,6 +74,10 @@ export default function GeneralSection(): React.ReactElement
|
||||
label={ i18n.t("options_page.general.options.show_notification") }
|
||||
checked={ notifyOnSave ?? false }
|
||||
onChange={ (_, e) => setNotifyOnSave(e.checked as boolean) } />
|
||||
<Checkbox
|
||||
label={ i18n.t("options_page.general.options.show_partial_save_notification") }
|
||||
checked={ showPartialSaveNotification ?? false }
|
||||
onChange={ (_, e) => setShowPartialSaveNotification(e.checked as boolean) } />
|
||||
<Checkbox
|
||||
label={ i18n.t("options_page.general.options.unload_tabs") }
|
||||
checked={ dismissOnLoad ?? false }
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components";
|
||||
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
|
||||
import { Unwatch } from "wxt/utils/storage";
|
||||
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
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
||||
?? ""
|
||||
);
|
||||
|
||||
const [color, setColor] = useState<chrome.tabGroups.ColorEnum | undefined | "pinned">(
|
||||
const [color, setColor] = useState<`${Browser.tabGroups.Color}` | undefined | "pinned">(
|
||||
props.type === "collection"
|
||||
? props.collection?.color :
|
||||
props.group?.pinned === true ? "pinned" : (props.group?.color ?? "blue")
|
||||
@@ -112,8 +112,8 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
||||
{ Object.keys(colorCls).map(i =>
|
||||
<fui.ToggleButton
|
||||
checked={ color === i }
|
||||
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
|
||||
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
|
||||
onClick={ () => setColor(i as `${Browser.tabGroups.Color}`) }
|
||||
className={ fui.mergeClasses(cls.colorButton, colorCls[i as `${Browser.tabGroups.Color}`]) }
|
||||
icon={ {
|
||||
className: cls.colorButton_icon,
|
||||
children: <Circle20Filled />
|
||||
@@ -121,7 +121,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
||||
key={ i }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
||||
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) }
|
||||
</fui.ToggleButton>
|
||||
) }
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import { Button, Caption1, makeStyles, mergeClasses, Subtitle2, tokens, Tooltip } from "@fluentui/react-components";
|
||||
import { Add20Filled, Add20Regular, bundleIcon } from "@fluentui/react-icons";
|
||||
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||
import CollectionMoreButton from "./CollectionMoreButton";
|
||||
import OpenCollectionButton from "./OpenCollectionButton";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
|
||||
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
|
||||
{
|
||||
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const { updateCollection } = useCollections();
|
||||
const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
|
||||
@@ -23,10 +23,16 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const newTabs: (TabItem | GroupItem)[] = isTab ?
|
||||
(await saveTabsToCollection(false)).items :
|
||||
await getSelectedTabs();
|
||||
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collection.timestamp);
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (newTabs.length > 0)
|
||||
await updateCollection({
|
||||
...collection,
|
||||
items: [...collection.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
|
||||
}, collection.timestamp);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
};
|
||||
|
||||
const cls = useStyles();
|
||||
@@ -59,7 +65,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
>
|
||||
{ tabCount < 1 ?
|
||||
<Button icon={ <AddIcon /> } appearance="subtle" onClick={ handleAddSelected }>
|
||||
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
|
||||
{ isTabView ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
|
||||
</Button>
|
||||
:
|
||||
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
|
||||
|
||||
@@ -3,21 +3,21 @@ import EditDialog from "@/entrypoints/sidepanel/components/EditDialog";
|
||||
import CollectionContext, { CollectionContextType } from "@/entrypoints/sidepanel/contexts/CollectionContext";
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import GroupContext, { GroupContextType } from "@/entrypoints/sidepanel/contexts/GroupContext";
|
||||
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
|
||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { openGroup } from "../../utils/opener";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
|
||||
export default function GroupMoreMenu(): ReactElement
|
||||
{
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const { group, indices } = useContext<GroupContextType>(GroupContext);
|
||||
const { hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const [deletePrompt] = useSettings("deletePrompt");
|
||||
@@ -67,10 +67,16 @@ export default function GroupMoreMenu(): ReactElement
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const newTabs: TabItem[] = isTab ?
|
||||
(await saveTabsToCollection(false)).items.flatMap(i => i.type === "tab" ? i : i.items) :
|
||||
await getSelectedTabs();
|
||||
updateGroup({ ...group, items: [...group.items, ...newTabs] }, collection.timestamp, indices[1]);
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (newTabs.length > 0)
|
||||
await updateGroup({
|
||||
...group,
|
||||
items: [...group.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
|
||||
}, collection.timestamp, indices[1]);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -90,7 +96,7 @@ export default function GroupMoreMenu(): ReactElement
|
||||
}
|
||||
|
||||
<MenuItem icon={ <AddIcon /> } onClick={ handleAddSelected }>
|
||||
{ isTab ? i18n.t("groups.menu.add_all") : i18n.t("groups.menu.add_selected") }
|
||||
{ isTabView ? i18n.t("groups.menu.add_all") : i18n.t("groups.menu.add_selected") }
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
|
||||
|
||||
@@ -43,12 +43,12 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
|
||||
sendMessage("refreshCollections", undefined);
|
||||
};
|
||||
|
||||
const addCollection = (collection: CollectionItem): void =>
|
||||
const addCollection = async (collection: CollectionItem): Promise<void> =>
|
||||
{
|
||||
updateStorage([collection, ...collections]);
|
||||
await updateStorage([collection, ...collections]);
|
||||
};
|
||||
|
||||
const removeItem = (...indices: number[]): void =>
|
||||
const removeItem = async (...indices: number[]): Promise<void> =>
|
||||
{
|
||||
const collectionIndex: number = collections.findIndex(i => i.timestamp === indices[0]);
|
||||
|
||||
@@ -59,34 +59,34 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
|
||||
else
|
||||
collections.splice(collectionIndex, 1);
|
||||
|
||||
updateStorage(collections);
|
||||
await updateStorage(collections);
|
||||
};
|
||||
|
||||
const updateCollections = (collectionList: CollectionItem[]): void =>
|
||||
const updateCollections = async (collectionList: CollectionItem[]): Promise<void> =>
|
||||
{
|
||||
updateStorage(collectionList);
|
||||
await updateStorage(collectionList);
|
||||
};
|
||||
|
||||
const updateCollection = (collection: CollectionItem, id: number): void =>
|
||||
const updateCollection = async (collection: CollectionItem, id: number): Promise<void> =>
|
||||
{
|
||||
const index: number = collections.findIndex(i => i.timestamp === id);
|
||||
collections[index] = collection;
|
||||
updateStorage(collections);
|
||||
await updateStorage(collections);
|
||||
};
|
||||
|
||||
const updateGroup = (group: GroupItem, collectionId: number, groupIndex: number): void =>
|
||||
const updateGroup = async (group: GroupItem, collectionId: number, groupIndex: number): Promise<void> =>
|
||||
{
|
||||
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
|
||||
collections[collectionIndex].items[groupIndex] = group;
|
||||
updateStorage(collections);
|
||||
await updateStorage(collections);
|
||||
};
|
||||
|
||||
const ungroup = (collectionId: number, groupIndex: number): void =>
|
||||
const ungroup = async (collectionId: number, groupIndex: number): Promise<void> =>
|
||||
{
|
||||
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
|
||||
const group = collections[collectionIndex].items[groupIndex] as GroupItem;
|
||||
collections[collectionIndex].items.splice(groupIndex, 1, ...group.items);
|
||||
updateStorage(collections);
|
||||
await updateStorage(collections);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -110,12 +110,12 @@ export type CollectionsContextType =
|
||||
tilesView: boolean;
|
||||
|
||||
refreshCollections: () => Promise<void>;
|
||||
addCollection: (collection: CollectionItem) => void;
|
||||
addCollection: (collection: CollectionItem) => Promise<void>;
|
||||
|
||||
updateCollections: (collections: CollectionItem[]) => void;
|
||||
updateCollection: (collection: CollectionItem, id: number) => void;
|
||||
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => void;
|
||||
ungroup: (collectionId: number, groupIndex: number) => void;
|
||||
updateCollections: (collections: CollectionItem[]) => Promise<void>;
|
||||
updateCollection: (collection: CollectionItem, id: number) => Promise<void>;
|
||||
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => Promise<void>;
|
||||
ungroup: (collectionId: number, groupIndex: number) => Promise<void>;
|
||||
|
||||
removeItem: (...indices: number[]) => void;
|
||||
removeItem: (...indices: number[]) => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -44,11 +44,11 @@ export default function FilterCollectionsButton({ value, onChange }: FilterColle
|
||||
<ColorIcon
|
||||
className={ fui.mergeClasses(
|
||||
cls.colorIcon,
|
||||
colorCls[i as chrome.tabGroups.ColorEnum]
|
||||
colorCls[i as `${Browser.tabGroups.Color}`]
|
||||
) } />
|
||||
}
|
||||
>
|
||||
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
||||
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) }
|
||||
</fui.MenuItemCheckbox>
|
||||
) }
|
||||
</fui.MenuList>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import resolveConflict from "@/features/collectionStorage/utils/resolveConflict";
|
||||
import { Button, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||
import { ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons";
|
||||
import { ArrowDownload20Regular, ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons";
|
||||
import { useCollections } from "../../../contexts/CollectionsProvider";
|
||||
import exportData from "@/entrypoints/options/utils/exportData";
|
||||
|
||||
export default function CloudIssueMessages(props: MessageBarProps): React.ReactElement
|
||||
{
|
||||
@@ -36,6 +37,9 @@ export default function CloudIssueMessages(props: MessageBarProps): React.ReactE
|
||||
{ i18n.t("merge_conflict_message.message") }
|
||||
</MessageBarBody>
|
||||
<MessageBarActions>
|
||||
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
|
||||
{ i18n.t("options_page.storage.export") }
|
||||
</Button>
|
||||
<Button icon={ <ArrowUpload20Regular /> } onClick={ () => overrideStorageWith("local") }>
|
||||
{ i18n.t("merge_conflict_message.accept_local") }
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||
import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||
|
||||
export default function StorageCapacityIssueMessage(props: MessageBarProps): JSX.Element
|
||||
export default function StorageCapacityIssueMessage(props: MessageBarProps): React.ReactElement
|
||||
{
|
||||
const { usedStorageRatio } = useStorageInfo();
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import { track } from "@/features/analytics";
|
||||
import useSettings, { SettingsValue } from "@/hooks/useSettings";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { closeTabsAsync } from "@/utils/closeTabsAsync";
|
||||
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import watchTabSelection from "@/utils/watchTabSelection";
|
||||
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
@@ -14,8 +19,26 @@ export default function ActionButton(): ReactElement
|
||||
|
||||
const handleAction = async (primary: boolean) =>
|
||||
{
|
||||
const colection = await saveTabsToCollection(primary === (defaultAction === "set_aside"));
|
||||
addCollection(colection);
|
||||
const [tabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (tabs.length < 1)
|
||||
{
|
||||
await sendPartialSaveNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
const collection: CollectionItem = await createCollectionFromTabs(tabs);
|
||||
await addCollection(collection);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
|
||||
const closeTabs: boolean = primary === (defaultAction === "set_aside");
|
||||
|
||||
if (closeTabs)
|
||||
await closeTabsAsync(tabs);
|
||||
|
||||
track(closeTabs ? "set_aside" : "save");
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { track } from "@/features/analytics";
|
||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
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();
|
||||
const permissions: Browser.permissions.Permissions = await browser.permissions.getAll();
|
||||
|
||||
if (!permissions.permissions?.includes("bookmarks"))
|
||||
{
|
||||
@@ -16,7 +15,7 @@ export default async function exportCollectionToBookmarks(collection: Collection
|
||||
return;
|
||||
}
|
||||
|
||||
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
||||
const rootFolder: Browser.bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
||||
title: getCollectionTitle(collection)
|
||||
});
|
||||
|
||||
|
||||
@@ -61,5 +61,5 @@ export default function filterCollections(
|
||||
export type CollectionFilterType =
|
||||
{
|
||||
query: string;
|
||||
colors: (chrome.tabGroups.ColorEnum | "none")[];
|
||||
colors: (`${Browser.tabGroups.Color}` | "none")[];
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { Tabs } from "wxt/browser";
|
||||
|
||||
export default async function getSelectedTabs(): Promise<TabItem[]>
|
||||
{
|
||||
let tabs: Tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
|
||||
let tabs: Browser.tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
|
||||
const tabCount: number = tabs.length;
|
||||
|
||||
tabs = tabs.filter(i =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { settings } from "@/utils/settings";
|
||||
import { Tabs, Windows } from "wxt/browser";
|
||||
|
||||
export async function openCollection(collection: CollectionItem, targetWindow?: "current" | "new" | "incognito"): Promise<void>
|
||||
{
|
||||
@@ -55,7 +54,7 @@ export async function openGroup(group: GroupItem, newWindow: boolean = false): P
|
||||
async function createGroup(group: GroupItem, windowId: number, discard?: boolean): Promise<void>
|
||||
{
|
||||
discard ??= await settings.dismissOnLoad.getValue();
|
||||
const tabs: Tabs.Tab[] = await Promise.all(group.items.map(async i =>
|
||||
const tabs: Browser.tabs.Tab[] = await Promise.all(group.items.map(async i =>
|
||||
await createTab(i.url, windowId, discard, group.pinned)
|
||||
));
|
||||
|
||||
@@ -63,21 +62,21 @@ async function createGroup(group: GroupItem, windowId: number, discard?: boolean
|
||||
if (group.pinned === true)
|
||||
return;
|
||||
|
||||
const groupId: number = await chrome.tabs.group({
|
||||
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!),
|
||||
const groupId: number = await browser.tabs.group({
|
||||
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!) as [number, ...number[]],
|
||||
createProperties: { windowId }
|
||||
});
|
||||
|
||||
await chrome.tabGroups.update(groupId, {
|
||||
await browser.tabGroups.update(groupId, {
|
||||
title: group.title,
|
||||
color: group.color
|
||||
});
|
||||
}
|
||||
|
||||
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Windows.CreateCreateDataType): Promise<void>
|
||||
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Browser.windows.CreateData): Promise<void>
|
||||
{
|
||||
const currentWindow: Windows.Window = windowProps ?
|
||||
await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }) :
|
||||
const currentWindow: Browser.windows.Window = windowProps ?
|
||||
(await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }))! :
|
||||
await browser.windows.getCurrent();
|
||||
const windowId: number = currentWindow.id!;
|
||||
|
||||
@@ -90,7 +89,7 @@ async function manageWindow(handle: (windowId: number) => Promise<void>, windowP
|
||||
await browser.tabs.remove(currentWindow.tabs![0].id!);
|
||||
}
|
||||
|
||||
async function createTab(url: string, windowId: number, discard: boolean, pinned?: boolean): Promise<Tabs.Tab>
|
||||
async function createTab(url: string, windowId: number, discard: boolean, pinned?: boolean): Promise<Browser.tabs.Tab>
|
||||
{
|
||||
const tab = await browser.tabs.create({ url, windowId: windowId, active: false, pinned });
|
||||
|
||||
@@ -102,7 +101,7 @@ async function createTab(url: string, windowId: number, discard: boolean, pinned
|
||||
|
||||
function discardOnLoad(tabId: number): void
|
||||
{
|
||||
const handleTabUpdated = (id: number, _: any, tab: Tabs.Tab) =>
|
||||
const handleTabUpdated = (id: number, _: any, tab: Browser.tabs.Tab) =>
|
||||
{
|
||||
if (id !== tabId || !tab.url)
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||
import { Unwatch, WatchCallback } from "wxt/utils/storage";
|
||||
import { analytics } from "./analytics";
|
||||
import { Permissions } from "wxt/browser";
|
||||
|
||||
const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
|
||||
{
|
||||
@@ -8,9 +7,8 @@ const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>
|
||||
{
|
||||
const isGranted: boolean = import.meta.env.FIREFOX
|
||||
? await browser.permissions.contains({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
})
|
||||
} as Browser.permissions.Permissions)
|
||||
: await allowAnalytics.getValue();
|
||||
|
||||
analytics.setEnabled(isGranted);
|
||||
@@ -30,14 +28,12 @@ const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>
|
||||
|
||||
if (value)
|
||||
result = await browser.permissions.request({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
});
|
||||
} as Browser.permissions.Permissions);
|
||||
else
|
||||
result = await browser.permissions.remove({
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
});
|
||||
} as Browser.permissions.Permissions);
|
||||
|
||||
if (!result)
|
||||
throw new Error("Permission request was denied");
|
||||
@@ -48,13 +44,14 @@ const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>
|
||||
if (!import.meta.env.FIREFOX)
|
||||
return allowAnalytics.watch(cb);
|
||||
|
||||
const listener = async (permissions: Permissions.Permissions): Promise<void> =>
|
||||
const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
|
||||
{
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
// @ts-expect-error Firefox-only API
|
||||
if (permissions.data_collection?.includes("technicalAndInteraction"))
|
||||
{
|
||||
// @ts-expect-error Introduced in Firefox 139
|
||||
const isGranted: boolean = await browser.permissions.contains({ data_collection: ["technicalAndInteraction"] });
|
||||
const isGranted: boolean = await browser.permissions.contains({
|
||||
data_collection: ["technicalAndInteraction"]
|
||||
} as Browser.permissions.Permissions);
|
||||
cb(isGranted, !isGranted);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export default async function getCollectionsFromCloud(): Promise<CollectionItem[
|
||||
const chunks: Record<string, string> =
|
||||
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
|
||||
|
||||
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "Base64" });
|
||||
const data: string = decompress(Object.values(chunks).join(""), { inputEncoding: "Base64" });
|
||||
|
||||
return parseCollections(data);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function parseCollection(data: string): CollectionItem
|
||||
return {
|
||||
type: "collection",
|
||||
timestamp: parseInt(data.match(/(?<=^c)\d+/)!.toString()),
|
||||
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
|
||||
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as `${Browser.tabGroups.Color}`,
|
||||
title: data.match(/(?<=^c[\da-z/]*\|).*/)?.toString(),
|
||||
items: []
|
||||
};
|
||||
@@ -64,7 +64,7 @@ function parseGroup(data: string): GroupItem
|
||||
return {
|
||||
type: "group",
|
||||
pinned: false,
|
||||
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
|
||||
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as `${Browser.tabGroups.Color}`,
|
||||
title: data.match(/(?<=^\tg\/[a-z]+\|).*$/)?.toString(),
|
||||
items: []
|
||||
};
|
||||
@@ -74,7 +74,7 @@ function parseTab(data: string): TabItem
|
||||
{
|
||||
return {
|
||||
type: "tab",
|
||||
url: data.match(/(?<=^(\t){1,2}t\|).*(?=\|)/)!.toString(),
|
||||
title: data.match(/(?<=^(\t){1,2}t\|.*\|).*$/)?.toString()
|
||||
url: data.match(/(?<=^\t{1,2}t\|).*(?=\|)/)!.toString(),
|
||||
title: data.match(/(?<=^\t{1,2}t\|.*\|).*$/)?.toString()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { trackError } from "@/features/analytics";
|
||||
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import saveCollectionsToCloud from "./saveCollectionsToCloud";
|
||||
import saveCollectionsToLocal from "./saveCollectionsToLocal";
|
||||
@@ -19,29 +17,8 @@ export default async function saveCollections(
|
||||
await saveCollectionsToLocal(collections, timestamp);
|
||||
|
||||
if (updateCloud && await collectionStorage.disableCloud.getValue() !== true)
|
||||
try
|
||||
{
|
||||
await saveCollectionsToCloud(collections, timestamp);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to save cloud storage");
|
||||
console.error(ex);
|
||||
trackError("cloud_save_error", ex as Error);
|
||||
|
||||
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.error_quota_exceeded.title"),
|
||||
message: i18n.t("notifications.error_quota_exceeded.message"),
|
||||
icon: "/notification_icons/cloud_error.png"
|
||||
});
|
||||
else
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.error_storage_full.title"),
|
||||
message: i18n.t("notifications.error_storage_full.message"),
|
||||
icon: "/notification_icons/cloud_error.png"
|
||||
});
|
||||
}
|
||||
|
||||
await updateGraphics(collections, graphicsCache);
|
||||
logger("Save complete");
|
||||
};
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { trackError } from "@/features/analytics";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { compress } from "lzutf8";
|
||||
import { WxtStorageItem } from "wxt/storage";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getChunkKeys from "./getChunkKeys";
|
||||
import serializeCollections from "./serializeCollections";
|
||||
|
||||
const logger = getLogger("saveCollectionsToCloud");
|
||||
|
||||
export default async function saveCollectionsToCloud(collections: CollectionItem[], timestamp: number): Promise<void>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!collections || collections.length < 1)
|
||||
{
|
||||
await collectionStorage.chunkCount.setValue(0);
|
||||
await browser.storage.sync.set({
|
||||
[getStorageKey(collectionStorage.chunkCount)]: 0,
|
||||
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
|
||||
});
|
||||
await browser.storage.sync.remove(getChunkKeys());
|
||||
return;
|
||||
}
|
||||
@@ -35,12 +44,32 @@ export default async function saveCollectionsToCloud(collections: CollectionItem
|
||||
if (chunks.length < collectionStorage.maxChunkCount)
|
||||
await browser.storage.sync.remove(getChunkKeys(chunks.length));
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to save cloud storage");
|
||||
console.error(ex);
|
||||
trackError("cloud_save_error", ex as Error);
|
||||
|
||||
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.error_quota_exceeded.title"),
|
||||
message: i18n.t("notifications.error_quota_exceeded.message"),
|
||||
icon: "/notification_icons/cloud_error.png"
|
||||
});
|
||||
else
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.error_storage_full.title"),
|
||||
message: i18n.t("notifications.error_storage_full.message"),
|
||||
icon: "/notification_icons/cloud_error.png"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function splitIntoChunks(data: string): string[]
|
||||
{
|
||||
// QUOTA_BYTES_PER_ITEM includes length of key name, length of content and 2 more bytes (for unknown reason).
|
||||
const chunkKey: string = getChunkKeys(collectionStorage.maxChunkCount - 1)[0];
|
||||
const chunkSize = (chrome.storage.sync.QUOTA_BYTES_PER_ITEM ?? 8192) - chunkKey.length - 2;
|
||||
const chunkSize = (browser.storage.sync.QUOTA_BYTES_PER_ITEM ?? 8192) - chunkKey.length - 2;
|
||||
const chunks: string[] = [];
|
||||
|
||||
for (let i = 0; i < data.length; i += chunkSize)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Permissions } from "wxt/browser";
|
||||
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||
import { Unwatch, WatchCallback } from "wxt/utils/storage";
|
||||
|
||||
const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
|
||||
{
|
||||
@@ -8,7 +7,7 @@ const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unkno
|
||||
|
||||
watch: (cb: WatchCallback<boolean>): Unwatch =>
|
||||
{
|
||||
const listener = async (permissions: Permissions.Permissions): Promise<void> =>
|
||||
const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
|
||||
{
|
||||
if (permissions.permissions?.includes("scripting") || permissions.origins?.includes("<all_urls>"))
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { githubLinks } from "@/data/links";
|
||||
import { analyticsPermission } from "@/features/analytics";
|
||||
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
import extLink from "@/utils/extLink";
|
||||
import * as fui from "@fluentui/react-components";
|
||||
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||
import { Unwatch } from "wxt/utils/storage";
|
||||
import { reviewSettings } from "../utils/setSettingsReviewNeeded";
|
||||
import { Unwatch } from "wxt/storage";
|
||||
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||
|
||||
export default function SettingsReviewDialog(): React.ReactElement
|
||||
{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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>
|
||||
export default async function setSettingsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<void>
|
||||
{
|
||||
const needsReview: string[] = await settingsForReview.getValue();
|
||||
|
||||
@@ -25,7 +24,7 @@ export const reviewSettings =
|
||||
THUMBNAILS: "thumbnails"
|
||||
};
|
||||
|
||||
async function checkAnalyticsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<boolean>
|
||||
async function checkAnalyticsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<boolean>
|
||||
{
|
||||
if (installReason === "install")
|
||||
return !await analyticsPermission.getValue();
|
||||
@@ -45,7 +44,7 @@ async function checkAnalyticsReviewNeeded(installReason: Runtime.OnInstalledReas
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkThumbnailsReviewNeeded(installReason: Runtime.OnInstalledReason, previousVersion?: string): Promise<boolean>
|
||||
async function checkThumbnailsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<boolean>
|
||||
{
|
||||
if (installReason === "install")
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { makeStyles, tokens } from "@fluentui/react-components";
|
||||
|
||||
export const useGroupColors: () => Record<chrome.tabGroups.ColorEnum, string> = makeStyles({
|
||||
export const useGroupColors: () => Record<`${Browser.tabGroups.Color}`, string> = makeStyles({
|
||||
blue:
|
||||
{
|
||||
"--border": tokens.colorPaletteBlueBorderActive,
|
||||
|
||||
@@ -14,8 +14,8 @@ export default function useStorageInfo(): StorageInfoHook
|
||||
|
||||
return {
|
||||
bytesInUse,
|
||||
storageQuota: chrome.storage.sync.QUOTA_BYTES ?? 102400,
|
||||
usedStorageRatio: bytesInUse / (chrome.storage.sync.QUOTA_BYTES ?? 102400)
|
||||
storageQuota: browser.storage.sync.QUOTA_BYTES ?? 102400,
|
||||
usedStorageRatio: bytesInUse / (browser.storage.sync.QUOTA_BYTES ?? 102400)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Ask for confirmation when deleting an item"
|
||||
show_badge: "Show counter badge"
|
||||
show_notification: "Show notification when saving tabs using context menu"
|
||||
show_partial_save_notification: "Show notification when some tabs couldn't be saved"
|
||||
unload_tabs: "Do not load tabs after opening"
|
||||
allow_analytics: "Allow collection of anonymous statistics"
|
||||
list_locations:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Pedir confirmación al eliminar un elemento"
|
||||
show_badge: "Mostrar insignia de contador"
|
||||
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
|
||||
show_partial_save_notification: "Mostrar notificación cuando algunas pestañas no se pudieron guardar"
|
||||
unload_tabs: "No cargar pestañas después de abrir"
|
||||
allow_analytics: "Permitir la recopilación de estadísticas anónimas"
|
||||
list_locations:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Chiedi conferma quando elimini un elemento"
|
||||
show_badge: "Mostra il badge del contatore"
|
||||
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
|
||||
show_partial_save_notification: "Mostra notifica quando alcune schede non sono state salvate"
|
||||
unload_tabs: "Non caricare le schede dopo l'apertura"
|
||||
allow_analytics: "Consenti la raccolta di statistiche anonime"
|
||||
list_locations:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Pytaj o potwierdzenie przy usuwaniu elementów"
|
||||
show_badge: "Pokaż licznik"
|
||||
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
|
||||
show_partial_save_notification: "Pokaż powiadomienie, jeśli niektóre karty nie zostały zapisane"
|
||||
unload_tabs: "Nie ładuj kart po otwarciu"
|
||||
allow_analytics: "Zezwól na zbieranie anonimowej statystyki"
|
||||
list_locations:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Pedir confirmação ao excluir um item"
|
||||
show_badge: "Mostrar contador no ícone"
|
||||
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
|
||||
show_partial_save_notification: "Mostrar notificação quando algumas abas não puderam ser salvas"
|
||||
unload_tabs: "Não carregar abas após abrir"
|
||||
allow_analytics: "Permitir coleta de estatísticas anônimas"
|
||||
list_locations:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Спрашивать подтверждение при удалении элементов"
|
||||
show_badge: "Показывать счетчик"
|
||||
show_notification: "Показывать уведомление при сохранении через контекстное меню"
|
||||
show_partial_save_notification: "Показывать уведомление, если некоторые вкладки не были сохранены"
|
||||
unload_tabs: "Не загружать вкладки после открытия"
|
||||
allow_analytics: "Разрешить сбор анонимной статистики"
|
||||
list_locations:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "Запитувати підтвердження при видаленні елементів"
|
||||
show_badge: "Показувати лічильник"
|
||||
show_notification: "Показувати сповіщення при збереженні через контекстне меню"
|
||||
show_partial_save_notification: "Показувати сповіщення, якщо деякі вкладки не були збережені"
|
||||
unload_tabs: "Не завантажувати вкладки після відкриття"
|
||||
allow_analytics: "Дозволити збір анонімної статистики"
|
||||
list_locations:
|
||||
|
||||
+53
-52
@@ -5,8 +5,8 @@ manifest:
|
||||
|
||||
shortcuts:
|
||||
toggle_sidebar: "打开收藏列表"
|
||||
set_aside: "将标签放到一边"
|
||||
save_tabs: "保存标签而不关闭"
|
||||
set_aside: "搁置标签页"
|
||||
save_tabs: "保存标签页但不关闭"
|
||||
|
||||
common:
|
||||
actions:
|
||||
@@ -14,10 +14,10 @@ common:
|
||||
save: "保存"
|
||||
close: "关闭"
|
||||
delete: "删除"
|
||||
reset_filters: "重置筛选器"
|
||||
reset_filters: "重置筛选"
|
||||
cta:
|
||||
feedback: "留下反馈"
|
||||
sponsor: "请我喝咖啡"
|
||||
sponsor: "请我喝杯咖啡!"
|
||||
tooltips:
|
||||
more: "更多"
|
||||
delete_prompt: "您确定吗?此操作无法撤销。"
|
||||
@@ -25,13 +25,13 @@ common:
|
||||
features:
|
||||
v3welcome:
|
||||
title: "欢迎使用搁置的标签页 3.0"
|
||||
text1: "我们很高兴宣布搁置的标签页扩展的新重大更新!"
|
||||
text1: "我们很高兴宣布搁置的标签页扩展新的重大更新!"
|
||||
text2: "此更新带来了全新的用户界面,以及许多新功能,包括:"
|
||||
list:
|
||||
item1: "支持标签组"
|
||||
item2: "收藏自定义"
|
||||
item3: "拖放重新排序和组织"
|
||||
item4: "从头开始手动创建收藏"
|
||||
item3: "拖放排序和整理"
|
||||
item4: "从零开始创建收藏"
|
||||
item5: "以及更多!"
|
||||
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
|
||||
actions:
|
||||
@@ -43,33 +43,33 @@ features:
|
||||
title: "这些统计数据将帮助我们改进扩展"
|
||||
p1: "我们只收集使用统计数据(收藏数量、使用的功能等)"
|
||||
p2: "我们不会收集您的任何数据!"
|
||||
p3_text: ""
|
||||
p3_link: "请参阅我们收集内容的完整列表"
|
||||
p3_text: "请参阅我们收集内容的"
|
||||
p3_link: "完整列表"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "新收藏已创建"
|
||||
message: "您的标签已保存到新收藏中"
|
||||
title: "已创建新收藏"
|
||||
message: "您的标签页已保存到新收藏中"
|
||||
error_quota_exceeded:
|
||||
title: "超出最大云写入操作"
|
||||
message: "我们已将您的标签保存到本地存储。您需要手动更新云存储"
|
||||
title: "超出最大云储存写入操作"
|
||||
message: "我们已将您的标签页保存到本地存储。您需要手动更新云存储"
|
||||
error_storage_full:
|
||||
title: "您的云存储已满"
|
||||
message: "我们已将您的标签保存到本地存储。请清理一些云存储空间"
|
||||
message: "我们已将您的标签页保存到本地存储。请清理一些云存储空间"
|
||||
bookmark_saved:
|
||||
title: "已导出到书签"
|
||||
message: "您的收藏已导出到书签"
|
||||
partial_save:
|
||||
title: "某些标签无法保存"
|
||||
message: "某些标签是我们无法访问的系统标签。它们已被跳过"
|
||||
title: "部分标签页无法保存"
|
||||
message: "部分标签页是无法访问的系统标签页。它们已被跳过"
|
||||
|
||||
actions:
|
||||
save:
|
||||
all: "保存所有标签"
|
||||
selected: "保存选定的标签"
|
||||
all: "保存所有标签页"
|
||||
selected: "保存选定的标签页"
|
||||
set_aside:
|
||||
all: "将所有标签放到一边"
|
||||
selected: "将选定的标签放到一边"
|
||||
all: "搁置所有标签页"
|
||||
selected: "搁置选定的标签页"
|
||||
show_collections: "显示收藏"
|
||||
|
||||
options_page:
|
||||
@@ -78,11 +78,12 @@ options_page:
|
||||
title: "常规"
|
||||
options:
|
||||
always_show_toolbars: "始终显示工具栏"
|
||||
include_pinned: "保存所有标签时包括固定标签"
|
||||
include_pinned: "保存所有标签页时包括已固定的标签页"
|
||||
show_delete_prompt: "删除项目时要求确认"
|
||||
show_badge: "显示计数徽章"
|
||||
show_notification: "使用上下文菜单保存标签时显示通知"
|
||||
unload_tabs: "打开后不加载标签"
|
||||
show_badge: "显示计数角标"
|
||||
show_notification: "使用上下文菜单保存标签页时显示通知"
|
||||
show_partial_save_notification: "如果某些标签页无法保存则显示通知"
|
||||
unload_tabs: "打开后不加载标签页"
|
||||
allow_analytics: "允许收集匿名统计数据"
|
||||
list_locations:
|
||||
title: "在以下位置打开收藏列表:"
|
||||
@@ -102,15 +103,15 @@ options_page:
|
||||
title: "默认操作"
|
||||
options:
|
||||
save_actions:
|
||||
title: "保存标签时的默认操作"
|
||||
title: "保存标签页时的默认操作"
|
||||
options:
|
||||
set_aside: "保存并关闭标签"
|
||||
save: "保存标签而不关闭"
|
||||
set_aside: "保存并关闭标签页"
|
||||
save: "保存标签页而不关闭"
|
||||
restore_actions:
|
||||
title: "打开收藏时的默认操作"
|
||||
options:
|
||||
open: "仅打开标签"
|
||||
restore: "打开标签并删除收藏"
|
||||
open: "仅打开标签页"
|
||||
restore: "打开标签页并删除收藏"
|
||||
storage:
|
||||
title: "存储"
|
||||
capacity:
|
||||
@@ -124,16 +125,16 @@ options_page:
|
||||
import_prompt:
|
||||
title: "导入数据"
|
||||
warning_title: "这是不可逆的操作"
|
||||
warning_text: "这将覆盖您的所有数据。请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
|
||||
warning_text: "这将覆盖您的所有数据!请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
|
||||
proceed: "选择文件"
|
||||
enable: "启用云存储"
|
||||
disable: "禁用云存储"
|
||||
disable_prompt:
|
||||
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
|
||||
action: "禁用并重新加载扩展"
|
||||
thumbnail_capture: "为已保存的标签保存缩略图和图标"
|
||||
thumbnail_capture: "为已保存的标签页保存缩略图和图标"
|
||||
thumbnail_capture_notice1: "需要访问已访问网站内容的权限"
|
||||
thumbnail_capture_notice2: "当你有大量集合时,禁用此功能可能会提高性能"
|
||||
thumbnail_capture_notice2: "有大量收藏时,禁用此功能可能会提高性能"
|
||||
clear_thumbnails:
|
||||
action: "删除已保存的图标"
|
||||
title: "删除缩略图和图标?"
|
||||
@@ -142,10 +143,10 @@ options_page:
|
||||
title: "关于"
|
||||
developed_by: "由尤金·福克斯开发"
|
||||
licensed_under: "许可协议"
|
||||
mit_license: "MIT 许可协议"
|
||||
mit_license: "MIT 协议"
|
||||
translation_cta:
|
||||
text: "发现错别字或想为您的语言提供翻译?"
|
||||
button: "从这里开始"
|
||||
button: "快速入门"
|
||||
links:
|
||||
website: "我的网站"
|
||||
source: "源代码"
|
||||
@@ -154,7 +155,7 @@ options_page:
|
||||
|
||||
collections:
|
||||
empty: "此收藏为空"
|
||||
tabs_count: "$1 个标签"
|
||||
tabs_count: "$1 个标签页"
|
||||
actions:
|
||||
open: "打开所有"
|
||||
restore: "恢复所有"
|
||||
@@ -170,35 +171,35 @@ collections:
|
||||
p1: "扩展需要权限才能在 InPrivate 窗口中打开标签"
|
||||
p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项"
|
||||
firefox:
|
||||
p1: "扩展需要权限才能在隐私窗口中打开标签"
|
||||
p1: "扩展需要权限才能在隐私窗口中打开标签页"
|
||||
p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”"
|
||||
chrome:
|
||||
p1: "扩展需要权限才能在隐身窗口中打开标签"
|
||||
p1: "扩展需要权限才能在隐身窗口中打开标签页"
|
||||
p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项"
|
||||
action: "设置"
|
||||
menu:
|
||||
delete: "删除收藏"
|
||||
add_selected: "添加选定的标签"
|
||||
add_all: "添加所有标签"
|
||||
add_group: "添加空组"
|
||||
add_selected: "添加选定的标签页"
|
||||
add_all: "添加所有标签页"
|
||||
add_group: "添加空分组"
|
||||
export_bookmarks: "导出到书签"
|
||||
edit: "编辑收藏"
|
||||
|
||||
groups:
|
||||
title: "组"
|
||||
title: "分组"
|
||||
pinned: "已固定"
|
||||
open: "打开所有"
|
||||
empty: "此组为空"
|
||||
empty: "此分组为空"
|
||||
menu:
|
||||
new_window: "在新窗口中打开"
|
||||
add_selected: "添加选定的标签"
|
||||
add_all: "添加所有标签"
|
||||
edit: "编辑组"
|
||||
add_selected: "添加选定的标签页"
|
||||
add_all: "添加所有标签页"
|
||||
edit: "编辑分组"
|
||||
ungroup: "取消分组"
|
||||
delete: "删除组"
|
||||
delete: "删除分组"
|
||||
|
||||
tabs:
|
||||
delete: "删除标签"
|
||||
delete: "删除标签页"
|
||||
|
||||
colors:
|
||||
none: "无颜色"
|
||||
@@ -217,8 +218,8 @@ dialogs:
|
||||
edit:
|
||||
title:
|
||||
edit_collection: "编辑收藏"
|
||||
edit_group: "编辑组"
|
||||
new_group: "新组"
|
||||
edit_group: "编辑分组"
|
||||
new_group: "新分组"
|
||||
new_collection: "新收藏"
|
||||
collection_title: "标题"
|
||||
color: "颜色"
|
||||
@@ -228,7 +229,7 @@ main:
|
||||
create_collection: "创建新收藏"
|
||||
menu:
|
||||
tiles_view: "平铺视图"
|
||||
changelog: "更新内容?"
|
||||
changelog: "更新内容"
|
||||
list:
|
||||
searchbar:
|
||||
title: "搜索"
|
||||
@@ -243,7 +244,7 @@ main:
|
||||
custom: "自定义"
|
||||
empty:
|
||||
title: "这里还没有内容"
|
||||
message: "将当前标签放到一边,或创建新收藏"
|
||||
message: "搁置当前标签页,或创建新收藏"
|
||||
empty_search:
|
||||
title: "未找到任何内容"
|
||||
message: "尝试更改搜索查询"
|
||||
@@ -265,5 +266,5 @@ parse_error_message:
|
||||
merge_conflict_message:
|
||||
title: "您的本地和云存储有冲突的更改。"
|
||||
message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。"
|
||||
accept_local: "用本地替换"
|
||||
accept_local: "采用本地替换云端"
|
||||
accept_cloud: "接受云端更改"
|
||||
|
||||
@@ -17,7 +17,7 @@ export type DefaultGroupItem =
|
||||
type: "group";
|
||||
pinned?: false;
|
||||
title?: string;
|
||||
color: chrome.tabGroups.ColorEnum;
|
||||
color: `${Browser.tabGroups.Color}`;
|
||||
items: TabItem[];
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export type CollectionItem =
|
||||
type: "collection";
|
||||
timestamp: number;
|
||||
title?: string;
|
||||
color?: chrome.tabGroups.ColorEnum;
|
||||
color?: `${Browser.tabGroups.Color}`;
|
||||
items: (TabItem | GroupItem)[];
|
||||
};
|
||||
|
||||
|
||||
+18
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tabs-aside",
|
||||
"private": true,
|
||||
"version": "3.1.0",
|
||||
"version": "3.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
@@ -16,31 +16,31 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fluentui/react-components": "^9.70.0",
|
||||
"@fluentui/react-icons": "^2.0.309",
|
||||
"@fluentui/react-components": "^9.72.6",
|
||||
"@fluentui/react-icons": "^2.0.313",
|
||||
"@webext-core/messaging": "^2.3.0",
|
||||
"@wxt-dev/analytics": "^0.4.1",
|
||||
"@wxt-dev/analytics": "^0.5.1",
|
||||
"@wxt-dev/i18n": "^0.2.4",
|
||||
"lzutf8": "^0.6.3",
|
||||
"react": "~18.3.1",
|
||||
"react-dom": "~18.3.1"
|
||||
"react": "~19.2.0",
|
||||
"react-dom": "~19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@eslint/css": "^0.14.1",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@eslint/json": "^0.14.0",
|
||||
"@stylistic/eslint-plugin": "^5.5.0",
|
||||
"@types/react": "~19.2.2",
|
||||
"@types/react-dom": "~19.2.2",
|
||||
"@wxt-dev/module-react": "^1.1.5",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.3.0",
|
||||
"globals": "^16.5.0",
|
||||
"scheduler": "0.23.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.42.0",
|
||||
"vite": "^7.1.5",
|
||||
"wxt": "~0.19.29"
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.2",
|
||||
"wxt": "^0.20.11"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export async function closeTabsAsync(tabs: Browser.tabs.Tab[]): Promise<void>
|
||||
{
|
||||
if (tabs.length < 1)
|
||||
return;
|
||||
|
||||
await browser.tabs.create({
|
||||
active: true,
|
||||
windowId: tabs[0].windowId
|
||||
});
|
||||
await browser.tabs.remove(tabs.map(i => i.id!));
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
|
||||
|
||||
export async function createCollectionFromTabs(tabs: Browser.tabs.Tab[]): Promise<CollectionItem>
|
||||
{
|
||||
const collection: CollectionItem = {
|
||||
type: "collection",
|
||||
timestamp: Date.now(),
|
||||
items: []
|
||||
};
|
||||
|
||||
if (tabs.length < 1)
|
||||
return collection;
|
||||
|
||||
let tabIndex: number = 0;
|
||||
|
||||
if (tabs[tabIndex].pinned)
|
||||
{
|
||||
collection.items.push({ type: "group", pinned: true, items: [] });
|
||||
|
||||
for (; tabIndex < tabs.length; tabIndex++)
|
||||
{
|
||||
if (!tabs[tabIndex].pinned)
|
||||
break;
|
||||
|
||||
(collection.items[0] as GroupItem).items.push({
|
||||
type: "tab",
|
||||
url: tabs[tabIndex].url!,
|
||||
title: tabs[tabIndex].title
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Special case, if all tabs are in the same group, create a collection with the group title
|
||||
if (tabs[0].groupId && tabs[0].groupId !== -1 &&
|
||||
tabs.every(i => i.groupId === tabs[0].groupId)
|
||||
)
|
||||
{
|
||||
const group = await browser.tabGroups.get(tabs[0].groupId);
|
||||
collection.title = group.title;
|
||||
collection.color = group.color;
|
||||
|
||||
tabs.forEach(i =>
|
||||
collection.items.push({ type: "tab", url: i.url!, title: i.title })
|
||||
);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
let activeGroup: number | null = null;
|
||||
|
||||
for (; tabIndex < tabs.length; tabIndex++)
|
||||
{
|
||||
const tab = tabs[tabIndex];
|
||||
|
||||
if (!tab.groupId || tab.groupId === -1)
|
||||
{
|
||||
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
|
||||
activeGroup = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!activeGroup || activeGroup !== tab.groupId)
|
||||
{
|
||||
activeGroup = tab.groupId;
|
||||
const group = await browser.tabGroups.get(activeGroup!);
|
||||
|
||||
collection.items.push({
|
||||
type: "group",
|
||||
color: group.color,
|
||||
title: group.title,
|
||||
items: []
|
||||
});
|
||||
}
|
||||
|
||||
(collection.items[collection.items.length - 1] as GroupItem).items.push({
|
||||
type: "tab",
|
||||
url: tab.url!,
|
||||
title: tab.title
|
||||
});
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { settings } from "./settings";
|
||||
|
||||
export async function getTabsToSaveAsync(): Promise<[Browser.tabs.Tab[], number]>
|
||||
{
|
||||
let tabs: Browser.tabs.Tab[] = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
highlighted: true
|
||||
});
|
||||
|
||||
if (tabs.length < 2)
|
||||
{
|
||||
const ignorePinned: boolean = await settings.ignorePinned.getValue();
|
||||
tabs = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
pinned: ignorePinned ? false : undefined
|
||||
});
|
||||
}
|
||||
|
||||
const tabsCount: number = tabs.length;
|
||||
const extension_prefix: string = browser.runtime.getURL("/");
|
||||
|
||||
tabs = tabs.filter(i =>
|
||||
i.url
|
||||
&& new URL(i.url).protocol !== "about:"
|
||||
&& new URL(i.url).hostname !== "newtab"
|
||||
&& !i.url!.startsWith(extension_prefix)
|
||||
);
|
||||
|
||||
return [tabs, tabsCount - tabs.length];
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { track } from "@/features/analytics";
|
||||
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
|
||||
import { Tabs } from "wxt/browser";
|
||||
import sendNotification from "./sendNotification";
|
||||
import { settings } from "./settings";
|
||||
|
||||
export default async function saveTabsToCollection(closeTabs: boolean): Promise<CollectionItem>
|
||||
{
|
||||
let tabs: Tabs.Tab[] = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
highlighted: true
|
||||
});
|
||||
|
||||
if (tabs.length < 2)
|
||||
{
|
||||
const ignorePinned: boolean = await settings.ignorePinned.getValue();
|
||||
tabs = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
pinned: ignorePinned ? false : undefined
|
||||
});
|
||||
}
|
||||
|
||||
const [collection, tabsToClose] = await createCollectionFromTabs(tabs);
|
||||
|
||||
if (closeTabs)
|
||||
{
|
||||
await browser.tabs.create({
|
||||
active: true,
|
||||
windowId: tabs[0].windowId
|
||||
});
|
||||
await browser.tabs.remove(tabsToClose.map(i => i.id!));
|
||||
}
|
||||
|
||||
track(closeTabs ? "set_aside" : "save");
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionItem, Tabs.Tab[]]>
|
||||
{
|
||||
if (tabs.length < 1)
|
||||
return [{ type: "collection", timestamp: Date.now(), items: [] }, []];
|
||||
|
||||
const tabCount: number = tabs.length;
|
||||
|
||||
tabs = tabs.filter(i =>
|
||||
i.url
|
||||
&& new URL(i.url).protocol !== "about:"
|
||||
&& new URL(i.url).hostname !== "newtab"
|
||||
);
|
||||
|
||||
if (tabs.length < tabCount)
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.partial_save.title"),
|
||||
message: i18n.t("notifications.partial_save.message"),
|
||||
icon: "/notification_icons/save_warning.png"
|
||||
});
|
||||
|
||||
tabs = tabs.filter(i => !i.url!.startsWith(browser.runtime.getURL("/")));
|
||||
|
||||
const collection: CollectionItem = {
|
||||
type: "collection",
|
||||
timestamp: Date.now(),
|
||||
items: []
|
||||
};
|
||||
|
||||
let tabIndex: number = 0;
|
||||
|
||||
if (tabs[tabIndex].pinned)
|
||||
{
|
||||
collection.items.push({ type: "group", pinned: true, items: [] });
|
||||
|
||||
for (; tabIndex < tabs.length; tabIndex++)
|
||||
{
|
||||
if (!tabs[tabIndex].pinned)
|
||||
break;
|
||||
|
||||
(collection.items[0] as GroupItem).items.push({
|
||||
type: "tab",
|
||||
url: tabs[tabIndex].url!,
|
||||
title: tabs[tabIndex].title
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Special case, if all tabs are in the same group, create a collection with the group title
|
||||
if (tabs[0].groupId && tabs[0].groupId !== -1 &&
|
||||
tabs.every(i => i.groupId === tabs[0].groupId)
|
||||
)
|
||||
{
|
||||
const group = await chrome.tabGroups.get(tabs[0].groupId);
|
||||
collection.title = group.title;
|
||||
collection.color = group.color;
|
||||
|
||||
tabs.forEach(i =>
|
||||
collection.items.push({ type: "tab", url: i.url!, title: i.title })
|
||||
);
|
||||
|
||||
return [collection, tabs];
|
||||
}
|
||||
|
||||
let activeGroup: number | null = null;
|
||||
|
||||
for (; tabIndex < tabs.length; tabIndex++)
|
||||
{
|
||||
const tab = tabs[tabIndex];
|
||||
|
||||
if (!tab.groupId || tab.groupId === -1)
|
||||
{
|
||||
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
|
||||
activeGroup = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!activeGroup || activeGroup !== tab.groupId)
|
||||
{
|
||||
activeGroup = tab.groupId;
|
||||
const group = await chrome.tabGroups.get(activeGroup);
|
||||
|
||||
collection.items.push({
|
||||
type: "group",
|
||||
color: group.color,
|
||||
title: group.title,
|
||||
items: []
|
||||
});
|
||||
}
|
||||
|
||||
(collection.items[collection.items.length - 1] as GroupItem).items.push({
|
||||
type: "tab",
|
||||
url: tab.url!,
|
||||
title: tab.title
|
||||
});
|
||||
}
|
||||
|
||||
return [collection, tabs];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import sendNotification from "./sendNotification";
|
||||
import { settings } from "./settings";
|
||||
|
||||
export default async function sendPartialSaveNotification(): Promise<void>
|
||||
{
|
||||
if (await settings.showPartialSaveNotification.getValue())
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.partial_save.title"),
|
||||
message: i18n.t("notifications.partial_save.message"),
|
||||
icon: "/notification_icons/save_warning.png"
|
||||
});
|
||||
}
|
||||
@@ -95,5 +95,13 @@ export const settings = {
|
||||
fallback: true,
|
||||
version: 1
|
||||
}
|
||||
),
|
||||
|
||||
showPartialSaveNotification: storage.defineItem<boolean>(
|
||||
"sync:showPartialSaveNotification",
|
||||
{
|
||||
fallback: true,
|
||||
version: 1
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Unwatch } from "wxt/storage";
|
||||
import { Unwatch } from "wxt/utils/storage";
|
||||
|
||||
export default function watchTabSelection(onChange: TabSelectChangeHandler): Unwatch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user