mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-07-02 19:52:47 +03:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cd3c4453d | |||
| 816c8bf28c |
+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: "/"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { track, trackError } from "@/features/analytics";
|
||||
import { collectionCount, getCollections, thumbnailCaptureEnabled, saveCollections } from "@/features/collectionStorage";
|
||||
import { collectionCount, getCollections, saveCollections, thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
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 getLogger from "@/utils/getLogger";
|
||||
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 { RemoveListenerCallback } from "@webext-core/messaging";
|
||||
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 { closeTabsAsync } from "@/utils/closeTabsAsync";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
|
||||
import getCollectionsFromLocal from "@/features/collectionStorage/utils/getCollectionsFromLocal";
|
||||
import { collectionStorage } from "@/features/collectionStorage/utils/collectionStorage";
|
||||
import getCollectionsFromCloud from "@/features/collectionStorage/utils/getCollectionsFromCloud";
|
||||
|
||||
export default defineBackground(() =>
|
||||
{
|
||||
@@ -36,16 +42,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(
|
||||
@@ -447,14 +479,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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -72,6 +73,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 }
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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,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(() =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -4,12 +4,22 @@ import { WxtStorageItem } from "wxt/storage";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getChunkKeys from "./getChunkKeys";
|
||||
import serializeCollections from "./serializeCollections";
|
||||
import { trackError } from "@/features/analytics";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
|
||||
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,6 +45,26 @@ 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[]
|
||||
{
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -82,6 +82,7 @@ options_page:
|
||||
show_delete_prompt: "删除项目时要求确认"
|
||||
show_badge: "显示计数角标"
|
||||
show_notification: "使用上下文菜单保存标签页时显示通知"
|
||||
show_partial_save_notification: "如果某些标签页无法保存则显示通知"
|
||||
unload_tabs: "打开后不加载标签页"
|
||||
allow_analytics: "允许收集匿名统计数据"
|
||||
list_locations:
|
||||
|
||||
+14
-14
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tabs-aside",
|
||||
"private": true,
|
||||
"version": "3.1.1",
|
||||
"version": "3.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
@@ -16,30 +16,30 @@
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fluentui/react-components": "^9.72.0",
|
||||
"@fluentui/react-icons": "^2.0.311",
|
||||
"@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": "~19.2.0",
|
||||
"react-dom": "~19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "^0.11.1",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@eslint/json": "^0.13.2",
|
||||
"@stylistic/eslint-plugin": "^5.4.0",
|
||||
"@types/react": "~19.2.0",
|
||||
"@types/react-dom": "~19.2.0",
|
||||
"@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.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"globals": "^16.5.0",
|
||||
"scheduler": "0.23.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^7.1.9",
|
||||
"typescript-eslint": "^8.46.4",
|
||||
"vite": "^7.2.2",
|
||||
"wxt": "~0.19.29"
|
||||
},
|
||||
"packageManager": "yarn@4.9.2"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Tabs } from "wxt/browser";
|
||||
|
||||
export async function closeTabsAsync(tabs: 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!));
|
||||
}
|
||||
@@ -1,69 +1,17 @@
|
||||
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>
|
||||
export async function createCollectionFromTabs(tabs: Tabs.Tab[]): 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: []
|
||||
};
|
||||
|
||||
if (tabs.length < 1)
|
||||
return collection;
|
||||
|
||||
let tabIndex: number = 0;
|
||||
|
||||
if (tabs[tabIndex].pinned)
|
||||
@@ -96,7 +44,7 @@ async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionIt
|
||||
collection.items.push({ type: "tab", url: i.url!, title: i.title })
|
||||
);
|
||||
|
||||
return [collection, tabs];
|
||||
return collection;
|
||||
}
|
||||
|
||||
let activeGroup: number | null = null;
|
||||
@@ -132,5 +80,5 @@ async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionIt
|
||||
});
|
||||
}
|
||||
|
||||
return [collection, tabs];
|
||||
return collection;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Tabs } from "wxt/browser";
|
||||
import { settings } from "./settings";
|
||||
|
||||
export async function getTabsToSaveAsync(): Promise<[Tabs.Tab[], number]>
|
||||
{
|
||||
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 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];
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user