1
0
mirror of https://github.com/XFox111/TabsAsideExtension.git synced 2026-07-02 19:52:47 +03:00

Compare commits

..

5 Commits

Author SHA1 Message Date
xfox111 41123bd8db fix: copilot fixes 2025-12-21 16:49:27 +03:00
xfox111 9fbc152a91 feat: add netscape bookmark import/export #203 2025-12-19 23:10:28 +03:00
xfox111 fdac0c0766 Patch 3.2.3 (#209)
* Bump the react group across 1 directory with 3 updates (#207)

Bumps the react group with 3 updates in the / directory: [react](https://github.com/facebook/react/tree/HEAD/packages/react), [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.0 to 19.2.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react)

Updates `@types/react` from 19.2.4 to 19.2.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.0 to 19.2.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.1/packages/react-dom)

Updates `@types/react` from 19.2.4 to 19.2.7
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: "@types/react"
  dependency-version: 19.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: react
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Bump the deps group across 1 directory with 5 updates (#208)

Bumps the deps group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@fluentui/react-components](https://github.com/microsoft/fluentui) | `9.72.7` | `9.72.8` |
| [@fluentui/react-icons](https://github.com/microsoft/fluentui-system-icons) | `2.0.314` | `2.0.316` |
| [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic/tree/HEAD/packages/eslint-plugin) | `5.5.0` | `5.6.1` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.46.4` | `8.49.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.2.2` | `7.2.7` |



Updates `@fluentui/react-components` from 9.72.7 to 9.72.8
- [Release notes](https://github.com/microsoft/fluentui/releases)
- [Commits](https://github.com/microsoft/fluentui/compare/@fluentui/react-components_v9.72.7...@fluentui/react-components_v9.72.8)

Updates `@fluentui/react-icons` from 2.0.314 to 2.0.316
- [Commits](https://github.com/microsoft/fluentui-system-icons/commits)

Updates `@stylistic/eslint-plugin` from 5.5.0 to 5.6.1
- [Release notes](https://github.com/eslint-stylistic/eslint-stylistic/releases)
- [Changelog](https://github.com/eslint-stylistic/eslint-stylistic/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint-stylistic/eslint-stylistic/commits/v5.6.1/packages/eslint-plugin)

Updates `typescript-eslint` from 8.46.4 to 8.49.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.49.0/packages/typescript-eslint)

Updates `vite` from 7.2.2 to 7.2.7
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.2.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.2.7/packages/vite)

---
updated-dependencies:
- dependency-name: "@fluentui/react-components"
  dependency-version: 9.72.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: "@fluentui/react-icons"
  dependency-version: 2.0.316
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: "@stylistic/eslint-plugin"
  dependency-version: 5.6.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: typescript-eslint
  dependency-version: 8.49.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: vite
  dependency-version: 7.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Migrated to npm

* Updated package.json version

* Fixed pipelines build

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 07:24:57 +03:00
xfox111 2065ee4637 fix: ",\t" adds to tab's title and URL on each cloud synchronization 2025-11-14 04:48:54 +03:00
xfox111 b51dd6083f chore(deps): WXT 0.20.0 bump + lockfile regen (#199)
* chore(deps): wxt 0.20.0 bump #134

* chore: 3.2.1 manifest bump
2025-11-14 02:16:57 +03:00
45 changed files with 11670 additions and 9815 deletions
+1 -1
View File
@@ -22,5 +22,5 @@
} }
}, },
"postCreateCommand": "yarn install" "postCreateCommand": "npm install"
} }
+3 -4
View File
@@ -51,8 +51,7 @@ jobs:
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: corepack enable - run: npm install
- run: yarn install
# Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043) # Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043)
- run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js - run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js
@@ -64,7 +63,7 @@ jobs:
working-directory: ./node_modules/@wxt-dev/analytics/dist working-directory: ./node_modules/@wxt-dev/analytics/dist
if: ${{ matrix.target == 'firefox' }} if: ${{ matrix.target == 'firefox' }}
- run: yarn zip -b ${{ matrix.target }} - run: npm run zip -- -b ${{ matrix.target }}
- name: Drop build artifacts (${{ matrix.target }}) - name: Drop build artifacts (${{ matrix.target }})
uses: actions/upload-artifact@main uses: actions/upload-artifact@main
@@ -81,7 +80,7 @@ jobs:
source: ./.output/firefox-mv3 source: ./.output/firefox-mv3
channel: listed channel: listed
- run: yarn npm audit - run: npm audit
continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }} continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }}
publish-github: publish-github:
+3 -4
View File
@@ -43,8 +43,7 @@ jobs:
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: corepack enable - run: npm install
- run: yarn install
# Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043) # Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043)
- run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js - run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js
@@ -56,7 +55,7 @@ jobs:
working-directory: ./node_modules/@wxt-dev/analytics/dist working-directory: ./node_modules/@wxt-dev/analytics/dist
if: ${{ matrix.target == 'firefox' }} if: ${{ matrix.target == 'firefox' }}
- run: yarn zip -b ${{ matrix.target }} - run: npm run zip -- -b ${{ matrix.target }}
- name: Drop artifacts (${{ matrix.target }}) - name: Drop artifacts (${{ matrix.target }})
uses: actions/upload-artifact@main uses: actions/upload-artifact@main
@@ -73,4 +72,4 @@ jobs:
source: ./.output/firefox-mv3 source: ./.output/firefox-mv3
channel: listed channel: listed
- run: yarn npm audit - run: npm audit
-117
View File
@@ -1,117 +0,0 @@
nodeLinker: node-modules
packageExtensions:
"@wxt-dev/module-react@*":
peerDependencies:
vite: "*"
"@fluentui/react-accordion@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-avatar@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-carousel@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-color-picker@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-combobox@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-dialog@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-field@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-list@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-menu@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-nav@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-overflow@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-popover@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-swatch-picker@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-table@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tabs@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tag-picker@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-teaching-popover@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-toolbar@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tree@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-alert@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-checkbox@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-components@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-drawer@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-infobutton@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-infolabel@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-input@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-persona@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-progress@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-radio@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-select@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-skeleton@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-slider@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-spinbutton@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-switch@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-tags@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-textarea@*":
peerDependencies:
scheduler: "0.23.0"
"@fluentui/react-search@*":
peerDependencies:
scheduler: "0.23.0"
+1 -1
View File
@@ -20,6 +20,6 @@ export const githubLinks =
export const storeLink: string = export const storeLink: string =
import.meta.env.FIREFOX import.meta.env.FIREFOX
? "https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/" : ? "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://microsoftedge.microsoft.com/addons/detail/tabs-aside/kmnblllmalkiapkfknnlpobmjjdnlhnd" :
"https://chromewebstore.google.com/detail/tabs-aside/mgmjbodjgijnebfgohlnjkegdpbdjgin"; "https://chromewebstore.google.com/detail/tabs-aside/mgmjbodjgijnebfgohlnjkegdpbdjgin";
+26 -24
View File
@@ -1,26 +1,25 @@
import { track, trackError } from "@/features/analytics"; import { track, trackError } from "@/features/analytics";
import { collectionCount, getCollections, saveCollections, thumbnailCaptureEnabled } 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 { migrateStorage } from "@/features/migration";
import { setSettingsReviewNeeded } from "@/features/settingsReview/utils"; import { setSettingsReviewNeeded } from "@/features/settingsReview/utils";
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog"; import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
import { SettingsValue } from "@/hooks/useSettings"; import { SettingsValue } from "@/hooks/useSettings";
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels"; import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
import { closeTabsAsync } from "@/utils/closeTabsAsync";
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
import getLogger from "@/utils/getLogger"; import getLogger from "@/utils/getLogger";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
import { onMessage, sendMessage } from "@/utils/messaging"; import { onMessage, sendMessage } from "@/utils/messaging";
import sendNotification from "@/utils/sendNotification"; import sendNotification from "@/utils/sendNotification";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification"; import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
import { settings } from "@/utils/settings"; import { settings } from "@/utils/settings";
import watchTabSelection from "@/utils/watchTabSelection"; import watchTabSelection from "@/utils/watchTabSelection";
import { RemoveListenerCallback } from "@webext-core/messaging"; import { RemoveListenerCallback } from "@webext-core/messaging";
import { Tabs, Windows } from "wxt/browser"; import { Unwatch } from "wxt/utils/storage";
import { Unwatch } from "wxt/storage";
import { openCollection, openGroup } from "./sidepanel/utils/opener"; import { openCollection, openGroup } from "./sidepanel/utils/opener";
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(() => export default defineBackground(() =>
{ {
@@ -99,7 +98,7 @@ export default defineBackground(() =>
let unwatchAddThumbnail: RemoveListenerCallback | null = null; let unwatchAddThumbnail: RemoveListenerCallback | null = null;
let captureInterval: NodeJS.Timeout | 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) if (!tab.url)
return; return;
@@ -111,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) if (!tab.url || tab.status !== "complete" || !tab.active)
return; return;
@@ -123,7 +122,7 @@ export default defineBackground(() =>
{ {
// We use chrome here because polyfill throws uncatchable errors for some reason // We use chrome here because polyfill throws uncatchable errors for some reason
// It's a compatible API anyway // 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) if (capture)
{ {
@@ -287,6 +286,7 @@ export default defineBackground(() =>
}; };
const toggleSidebarFirefox = async (): Promise<void> => const toggleSidebarFirefox = async (): Promise<void> =>
// @ts-expect-error Firefox-only API
await browser.sidebarAction.toggle(); await browser.sidebarAction.toggle();
const updateButton = async (action: SettingsValue<"contextAction">): Promise<void> => const updateButton = async (action: SettingsValue<"contextAction">): Promise<void> =>
@@ -303,7 +303,7 @@ export default defineBackground(() =>
unwatchActionTitle?.(); unwatchActionTitle?.();
if (!import.meta.env.FIREFOX) if (!import.meta.env.FIREFOX)
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); await browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
// Setup new behavior // Setup new behavior
if (action === "action") if (action === "action")
@@ -322,7 +322,7 @@ export default defineBackground(() =>
if (import.meta.env.FIREFOX) if (import.meta.env.FIREFOX)
browser.action.onClicked.addListener(toggleSidebarFirefox); browser.action.onClicked.addListener(toggleSidebarFirefox);
else else
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
} }
else if (location !== "popup") else if (location !== "popup")
browser.action.onClicked.addListener(openCollectionsInTab); browser.action.onClicked.addListener(openCollectionsInTab);
@@ -341,17 +341,17 @@ export default defineBackground(() =>
{ {
logger("enforcePinnedTab"); 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) for (const openWindow of openWindows)
{ {
if (openWindow.incognito || openWindow.type !== "normal") if (openWindow.incognito || openWindow.type !== "normal")
continue; 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")); 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) if (!targetTab)
await browser.tabs.create({ await browser.tabs.create({
@@ -361,7 +361,7 @@ export default defineBackground(() =>
pinned: true 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) if (tabsToClose.length > 0)
await browser.tabs.remove(tabsToClose.map(tab => tab.id!)); await browser.tabs.remove(tabsToClose.map(tab => tab.id!));
@@ -373,7 +373,7 @@ export default defineBackground(() =>
logger("updateView", viewLocation); logger("updateView", viewLocation);
browser.tabs.onHighlighted.removeListener(enforcePinnedTab); 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") url: browser.runtime.getURL("/sidepanel.html")
}); });
await browser.tabs.remove(tabs.map(tab => tab.id!)); await browser.tabs.remove(tabs.map(tab => tab.id!));
@@ -383,11 +383,12 @@ export default defineBackground(() =>
}); });
if (import.meta.env.FIREFOX) if (import.meta.env.FIREFOX)
// @ts-expect-error Firefox-only API
await browser.sidebarAction.setPanel({ await browser.sidebarAction.setPanel({
panel: viewLocation === "sidebar" ? browser.runtime.getURL("/sidepanel.html") : "" panel: viewLocation === "sidebar" ? browser.runtime.getURL("/sidepanel.html") : ""
}); });
else else
await chrome.sidePanel.setOptions({ enabled: viewLocation === "sidebar" }); await browser.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
if (viewLocation === "pinned") if (viewLocation === "pinned")
{ {
@@ -418,9 +419,10 @@ export default defineBackground(() =>
if (view === "sidebar") if (view === "sidebar")
{ {
if (import.meta.env.FIREFOX) if (import.meta.env.FIREFOX)
// @ts-expect-error Firefox-only API
browser.sidebarAction.open(); browser.sidebarAction.open();
else else
chrome.sidePanel.open({ windowId }); browser.sidePanel.open({ windowId });
} }
else else
browser.action.openPopup(); browser.action.openPopup();
@@ -430,11 +432,11 @@ export default defineBackground(() =>
{ {
logger("openCollectionsInTab"); 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) 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 => availableWindows = availableWindows.filter(window =>
!window.incognito && !window.incognito &&
@@ -443,7 +445,7 @@ export default defineBackground(() =>
if (availableWindows.length > 0) 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") tab => tab.url === browser.runtime.getURL("/sidepanel.html")
)!; )!;
@@ -460,7 +462,7 @@ export default defineBackground(() =>
} }
else 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") tab => tab.url === browser.runtime.getURL("/sidepanel.html")
); );
@@ -41,5 +41,9 @@ export const useOptionsStyles = makeStyles({
flexFlow: "column", flexFlow: "column",
alignItems: "flex-start", alignItems: "flex-start",
gap: tokens.spacingVerticalSNudge gap: tokens.spacingVerticalSNudge
},
messageBar:
{
flexShrink: 0
} }
}); });
@@ -1,8 +1,8 @@
import { analyticsPermission } from "@/features/analytics";
import useSettings, { SettingsValue } from "@/hooks/useSettings"; import useSettings, { SettingsValue } from "@/hooks/useSettings";
import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components"; import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
import { KeyCommand20Regular } from "@fluentui/react-icons"; import { KeyCommand20Regular } from "@fluentui/react-icons";
import { useOptionsStyles } from "../hooks/useOptionsStyles"; import { useOptionsStyles } from "../hooks/useOptionsStyles";
import { analyticsPermission } from "@/features/analytics";
export default function GeneralSection(): React.ReactElement export default function GeneralSection(): React.ReactElement
{ {
@@ -45,6 +45,7 @@ export default function GeneralSection(): React.ReactElement
setContextAction("open"); setContextAction("open");
if (import.meta.env.FIREFOX && e.optionValue !== "sidebar") if (import.meta.env.FIREFOX && e.optionValue !== "sidebar")
// @ts-expect-error Firefox-only API
browser.sidebarAction.close(); browser.sidebarAction.close();
setListLocation(e.optionValue as ListLocationType); setListLocation(e.optionValue as ListLocationType);
+58 -48
View File
@@ -2,12 +2,13 @@ import { useDialog } from "@/contexts/DialogProvider";
import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage"; import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage";
import { useDangerStyles } from "@/hooks/useDangerStyles"; import { useDangerStyles } from "@/hooks/useDangerStyles";
import useStorageInfo from "@/hooks/useStorageInfo"; import useStorageInfo from "@/hooks/useStorageInfo";
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components"; import { Button, Divider, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Subtitle2, Switch } from "@fluentui/react-components";
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons"; import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
import { Unwatch } from "wxt/utils/storage";
import { useOptionsStyles } from "../hooks/useOptionsStyles"; import { useOptionsStyles } from "../hooks/useOptionsStyles";
import exportData from "../utils/exportData"; import exportData from "../utils/exportData";
import importData from "../utils/importData"; import importData from "../utils/importData";
import { Unwatch } from "wxt/storage"; import BookmarksSection from "@/features/netscapeBookmarks/layouts/BookmarksSection";
export default function StorageSection(): React.ReactElement export default function StorageSection(): React.ReactElement
{ {
@@ -78,6 +79,59 @@ export default function StorageSection(): React.ReactElement
return ( return (
<> <>
<Subtitle2>{ i18n.t("options_page.storage.manage_title") }</Subtitle2>
{ isCloudDisabled === false &&
<Field
label={ i18n.t("options_page.storage.capacity.title") }
hint={ i18n.t("options_page.storage.capacity.description", [(bytesInUse / 1024).toFixed(1), storageQuota / 1024]) }
validationState={ usedStorageRatio >= 0.8 ? "error" : undefined }
>
<ProgressBar value={ usedStorageRatio } thickness="large" />
</Field>
}
<div className={ cls.horizontalButtons }>
{ isCloudDisabled === true &&
<Button appearance="primary" onClick={ () => setCloudStorage(true) }>
{ i18n.t("options_page.storage.enable") }
</Button>
}
{ isCloudDisabled === false &&
<div className={ cls.horizontalButtons }>
<Button
appearance="subtle" className={ dangerCls.buttonSubtle }
onClick={ handleDisableCloud }
>
{ i18n.t("options_page.storage.disable") }
</Button>
</div>
}
</div>
<div className={ cls.horizontalButtons }>
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
{ i18n.t("options_page.storage.export") }
</Button>
<Button icon={ <ArrowUpload20Regular /> } onClick={ handleImport }>
{ i18n.t("options_page.storage.import") }
</Button>
</div>
{ importResult !== null &&
<MessageBar intent={ importResult ? "success" : "error" } className={ cls.messageBar }>
<MessageBarBody>
{ importResult === true ?
i18n.t("options_page.storage.import_results.success") :
i18n.t("options_page.storage.import_results.error")
}
</MessageBarBody>
</MessageBar>
}
<Divider />
<Subtitle2>{ i18n.t("options_page.storage.thumbnails_title") }</Subtitle2>
<div className={ cls.group }> <div className={ cls.group }>
<Switch <Switch
checked={ isThumbnailCaptureEnabled ?? true } checked={ isThumbnailCaptureEnabled ?? true }
@@ -101,52 +155,8 @@ export default function StorageSection(): React.ReactElement
</Button> </Button>
</div> </div>
{ isCloudDisabled === false && <Divider />
<Field <BookmarksSection />
label={ i18n.t("options_page.storage.capacity.title") }
hint={ i18n.t("options_page.storage.capacity.description", [(bytesInUse / 1024).toFixed(1), storageQuota / 1024]) }
validationState={ usedStorageRatio >= 0.8 ? "error" : undefined }
>
<ProgressBar value={ usedStorageRatio } thickness="large" />
</Field>
}
{ isCloudDisabled === true &&
<Button appearance="primary" onClick={ () => setCloudStorage(true) }>
{ i18n.t("options_page.storage.enable") }
</Button>
}
<div className={ cls.horizontalButtons }>
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
{ i18n.t("options_page.storage.export") }
</Button>
<Button icon={ <ArrowUpload20Regular /> } onClick={ handleImport }>
{ i18n.t("options_page.storage.import") }
</Button>
</div>
{ importResult !== null &&
<MessageBar intent={ importResult ? "success" : "error" }>
<MessageBarBody>
{ importResult === true ?
i18n.t("options_page.storage.import_results.success") :
i18n.t("options_page.storage.import_results.error")
}
</MessageBarBody>
</MessageBar>
}
{ isCloudDisabled === false &&
<div className={ cls.horizontalButtons }>
<Button
appearance="subtle" className={ dangerCls.buttonSubtle }
onClick={ handleDisableCloud }
>
{ i18n.t("options_page.storage.disable") }
</Button>
</div>
}
</> </>
); );
} }
@@ -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.type === "collection"
? props.collection?.color : ? props.collection?.color :
props.group?.pinned === true ? "pinned" : (props.group?.color ?? "blue") 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 => { Object.keys(colorCls).map(i =>
<fui.ToggleButton <fui.ToggleButton
checked={ color === i } checked={ color === i }
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) } onClick={ () => setColor(i as `${Browser.tabGroups.Color}`) }
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) } className={ fui.mergeClasses(cls.colorButton, colorCls[i as `${Browser.tabGroups.Color}`]) }
icon={ { icon={ {
className: cls.colorButton_icon, className: cls.colorButton_icon,
children: <Circle20Filled /> children: <Circle20Filled />
@@ -121,7 +121,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
key={ i } key={ i }
shape="circular" shape="circular"
> >
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) } { i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) }
</fui.ToggleButton> </fui.ToggleButton>
) } ) }
</div> </div>
@@ -44,11 +44,11 @@ export default function FilterCollectionsButton({ value, onChange }: FilterColle
<ColorIcon <ColorIcon
className={ fui.mergeClasses( className={ fui.mergeClasses(
cls.colorIcon, 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.MenuItemCheckbox>
) } ) }
</fui.MenuList> </fui.MenuList>
@@ -1,12 +1,11 @@
import { track } from "@/features/analytics";
import { CollectionItem, TabItem } from "@/models/CollectionModels"; import { CollectionItem, TabItem } from "@/models/CollectionModels";
import sendNotification from "@/utils/sendNotification"; import sendNotification from "@/utils/sendNotification";
import { Bookmarks, Permissions } from "wxt/browser";
import { getCollectionTitle } from "./getCollectionTitle"; import { getCollectionTitle } from "./getCollectionTitle";
import { track } from "@/features/analytics";
export default async function exportCollectionToBookmarks(collection: CollectionItem) 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")) if (!permissions.permissions?.includes("bookmarks"))
{ {
@@ -16,7 +15,7 @@ export default async function exportCollectionToBookmarks(collection: Collection
return; return;
} }
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({ const rootFolder: Browser.bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
title: getCollectionTitle(collection) title: getCollectionTitle(collection)
}); });
@@ -61,5 +61,5 @@ export default function filterCollections(
export type CollectionFilterType = export type CollectionFilterType =
{ {
query: string; query: string;
colors: (chrome.tabGroups.ColorEnum | "none")[]; colors: (`${Browser.tabGroups.Color}` | "none")[];
}; };
@@ -1,10 +1,9 @@
import { TabItem } from "@/models/CollectionModels"; import { TabItem } from "@/models/CollectionModels";
import sendNotification from "@/utils/sendNotification"; import sendNotification from "@/utils/sendNotification";
import { Tabs } from "wxt/browser";
export default async function getSelectedTabs(): Promise<TabItem[]> 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; const tabCount: number = tabs.length;
tabs = tabs.filter(i => tabs = tabs.filter(i =>
+9 -10
View File
@@ -1,7 +1,6 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle"; import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels"; import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
import { settings } from "@/utils/settings"; import { settings } from "@/utils/settings";
import { Tabs, Windows } from "wxt/browser";
export async function openCollection(collection: CollectionItem, targetWindow?: "current" | "new" | "incognito"): Promise<void> 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> async function createGroup(group: GroupItem, windowId: number, discard?: boolean): Promise<void>
{ {
discard ??= await settings.dismissOnLoad.getValue(); 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) 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) if (group.pinned === true)
return; return;
const groupId: number = await chrome.tabs.group({ const groupId: number = await browser.tabs.group({
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!), tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!) as [number, ...number[]],
createProperties: { windowId } createProperties: { windowId }
}); });
await chrome.tabGroups.update(groupId, { await browser.tabGroups.update(groupId, {
title: group.title, title: group.title,
color: group.color 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 ? const currentWindow: Browser.windows.Window = windowProps ?
await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }) : (await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }))! :
await browser.windows.getCurrent(); await browser.windows.getCurrent();
const windowId: number = currentWindow.id!; 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!); 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 }); 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 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) if (id !== tabId || !tab.url)
return; return;
+2 -1
View File
@@ -19,13 +19,14 @@ export default defineConfig([
{ files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] }, { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] },
{ {
files: ["**/*.{jsonc,json}"], files: ["**/*.{jsonc,json}"],
ignores: [".devcontainer/devcontainer.json", "package-lock.json"],
plugins: { json }, plugins: { json },
language: "json/jsonc", language: "json/jsonc",
extends: ["json/recommended"] extends: ["json/recommended"]
}, },
{ {
files: ["**/*.json"], files: ["**/*.json"],
ignores: [".devcontainer/devcontainer.json"], ignores: [".devcontainer/devcontainer.json", "package-lock.json"],
plugins: { json }, plugins: { json },
language: "json/json", language: "json/json",
extends: ["json/recommended"] extends: ["json/recommended"]
@@ -1,6 +1,5 @@
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage"; import { Unwatch, WatchCallback } from "wxt/utils/storage";
import { analytics } from "./analytics"; import { analytics } from "./analytics";
import { Permissions } from "wxt/browser";
const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> = const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
{ {
@@ -9,7 +8,7 @@ const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>
const isGranted: boolean = import.meta.env.FIREFOX const isGranted: boolean = import.meta.env.FIREFOX
? await browser.permissions.contains({ ? await browser.permissions.contains({
data_collection: ["technicalAndInteraction"] data_collection: ["technicalAndInteraction"]
}) } as Browser.permissions.Permissions)
: await allowAnalytics.getValue(); : await allowAnalytics.getValue();
analytics.setEnabled(isGranted); analytics.setEnabled(isGranted);
@@ -30,11 +29,11 @@ const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>
if (value) if (value)
result = await browser.permissions.request({ result = await browser.permissions.request({
data_collection: ["technicalAndInteraction"] data_collection: ["technicalAndInteraction"]
}); } as Browser.permissions.Permissions);
else else
result = await browser.permissions.remove({ result = await browser.permissions.remove({
data_collection: ["technicalAndInteraction"] data_collection: ["technicalAndInteraction"]
}); } as Browser.permissions.Permissions);
if (!result) if (!result)
throw new Error("Permission request was denied"); throw new Error("Permission request was denied");
@@ -45,11 +44,14 @@ const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>
if (!import.meta.env.FIREFOX) if (!import.meta.env.FIREFOX)
return allowAnalytics.watch(cb); return allowAnalytics.watch(cb);
const listener = async (permissions: Permissions.Permissions): Promise<void> => const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
{ {
// @ts-expect-error Firefox-only API
if (permissions.data_collection?.includes("technicalAndInteraction")) if (permissions.data_collection?.includes("technicalAndInteraction"))
{ {
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); cb(isGranted, !isGranted);
} }
}; };
@@ -44,7 +44,7 @@ function parseCollection(data: string): CollectionItem
return { return {
type: "collection", type: "collection",
timestamp: parseInt(data.match(/(?<=^c)\d+/)!.toString()), 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(), title: data.match(/(?<=^c[\da-z/]*\|).*/)?.toString(),
items: [] items: []
}; };
@@ -64,7 +64,7 @@ function parseGroup(data: string): GroupItem
return { return {
type: "group", type: "group",
pinned: false, 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(), title: data.match(/(?<=^\tg\/[a-z]+\|).*$/)?.toString(),
items: [] items: []
}; };
@@ -74,7 +74,7 @@ function parseTab(data: string): TabItem
{ {
return { return {
type: "tab", type: "tab",
url: data.match(/(?<=^(\t){1,2}t\|).*(?=\|)/)!.toString(), url: data.match(/(?<=^\t{1,2}t\|).*(?=\|)/)!.toString(),
title: data.match(/(?<=^(\t){1,2}t\|.*\|).*$/)?.toString() title: data.match(/(?<=^\t{1,2}t\|.*\|).*$/)?.toString()
}; };
} }
@@ -1,12 +1,11 @@
import { trackError } from "@/features/analytics";
import { CollectionItem } from "@/models/CollectionModels"; import { CollectionItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import sendNotification from "@/utils/sendNotification";
import { compress } from "lzutf8"; import { compress } from "lzutf8";
import { WxtStorageItem } from "wxt/storage";
import { collectionStorage } from "./collectionStorage"; import { collectionStorage } from "./collectionStorage";
import getChunkKeys from "./getChunkKeys"; import getChunkKeys from "./getChunkKeys";
import serializeCollections from "./serializeCollections"; import serializeCollections from "./serializeCollections";
import { trackError } from "@/features/analytics";
import sendNotification from "@/utils/sendNotification";
import getLogger from "@/utils/getLogger";
const logger = getLogger("saveCollectionsToCloud"); const logger = getLogger("saveCollectionsToCloud");
@@ -70,7 +69,7 @@ function splitIntoChunks(data: string): string[]
{ {
// QUOTA_BYTES_PER_ITEM includes length of key name, length of content and 2 more bytes (for unknown reason). // 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 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[] = []; const chunks: string[] = [];
for (let i = 0; i < data.length; i += chunkSize) for (let i = 0; i < data.length; i += chunkSize)
@@ -1,5 +1,4 @@
import { Permissions } from "wxt/browser"; import { Unwatch, WatchCallback } from "wxt/utils/storage";
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> = 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 => 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>")) if (permissions.permissions?.includes("scripting") || permissions.origins?.includes("<all_urls>"))
{ {
@@ -0,0 +1,66 @@
import { useDialog } from "@/contexts/DialogProvider";
import { Body1, Button, makeStyles, MessageBar, MessageBarBody, Subtitle2, tokens } from "@fluentui/react-components";
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
import importBookmarks from "../utils/importBookmarks";
import exportBookmarks from "../utils/exportBookmarks";
export default function BookmarksSection(): React.ReactElement
{
const cls = useStyles();
const dialog = useDialog();
const [importResult, setImportResult] = useState<number | null>(null);
const handleImport = (): void =>
dialog.pushPrompt({
title: i18n.t("features.netscape_bookmarks.import_dialog.title"),
confirmText: i18n.t("options_page.storage.import_prompt.proceed"),
onConfirm: () => importBookmarks().then(setImportResult),
content: (
<Body1 as="p">
{ i18n.t("features.netscape_bookmarks.import_dialog.content") }
</Body1>
)
});
return (
<div className={ cls.root }>
<Subtitle2>{ i18n.t("features.netscape_bookmarks.title") }</Subtitle2>
{ importResult !== null &&
<MessageBar intent={ importResult >= 0 ? "success" : "error" } layout="multiline">
<MessageBarBody>
{ importResult >= 0 ?
i18n.t("features.netscape_bookmarks.import_result.success", [importResult]) :
i18n.t("features.netscape_bookmarks.import_result.error")
}
</MessageBarBody>
</MessageBar>
}
<div className={ cls.buttons }>
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportBookmarks }>
{ i18n.t("features.netscape_bookmarks.export") }
</Button>
<Button icon={ <ArrowUpload20Regular /> } onClick={ handleImport }>
{ i18n.t("features.netscape_bookmarks.import") }
</Button>
</div>
</div>
);
}
const useStyles = makeStyles({
root:
{
display: "flex",
flexFlow: "column",
gap: tokens.spacingVerticalMNudge
},
buttons:
{
display: "flex",
flexWrap: "wrap",
gap: tokens.spacingVerticalSNudge
}
});
@@ -0,0 +1,104 @@
import { CollectionItem, GraphicsStorage, GroupItem, TabItem } from "@/models/CollectionModels";
import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark";
export default function convertBookmarks(bookmarks: Bookmark[]): [CollectionItem[], GraphicsStorage, number]
{
let count: number = 0;
const graphics: GraphicsStorage = {};
const items: CollectionItem[] = [];
const untitled: CollectionItem = {
items: [],
timestamp: Date.now(),
type: "collection"
};
for (const bookmark of bookmarks)
{
if (bookmark.type === "bookmark")
{
untitled.items.push(getTab(bookmark, graphics));
count++;
}
else if (bookmark.type === "folder")
{
const collection: CollectionItem = getCollection(bookmark, graphics);
items.push(collection);
count += collection.items.reduce((acc, item) =>
{
if (item.type === "tab")
return acc + 1;
else if (item.type === "group")
return acc + item.items.length;
return acc;
}, 0);
}
}
if (untitled.items.length > 0)
items.unshift(untitled);
return [items, graphics, count];
}
function getTab(bookmark: Bookmark, graphics: GraphicsStorage): TabItem
{
if (bookmark.icon)
graphics[bookmark.url!] = {
icon: bookmark.icon
};
return {
type: "tab",
url: bookmark.url!,
title: bookmark.title || bookmark.url!
};
}
function getCollection(bookmark: Bookmark, graphics: GraphicsStorage): CollectionItem
{
const collection: CollectionItem = {
items: [],
title: bookmark.title,
timestamp: Date.now(),
type: "collection"
};
if (bookmark.children)
for (const child of bookmark.children)
{
if (child.type === "bookmark")
collection.items.push(getTab(child, graphics));
else if (child.type === "folder" && child.children)
collection.items.push(getGroup(child, graphics));
}
return collection;
}
function getGroup(bookmark: Bookmark, graphics: GraphicsStorage): GroupItem
{
const group: GroupItem = {
items: [],
title: bookmark.title,
pinned: false,
type: "group",
color: getRandomColor()
};
if (bookmark.children)
for (const child of bookmark.children)
{
if (child.type === "bookmark")
group.items.push(getTab(child, graphics));
else if (child.type === "folder")
group.items.push(...getGroup(child, graphics).items);
}
return group;
}
function getRandomColor(): "blue" | "cyan" | "green" | "grey" | "orange" | "pink" | "purple" | "red" | "yellow"
{
const colors = ["blue", "cyan", "green", "grey", "orange", "pink", "purple", "red", "yellow"] as const;
return colors[Math.floor(Math.random() * colors.length)];
}
@@ -0,0 +1,69 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import { getCollections } from "@/features/collectionStorage";
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
export default async function exportBookmarks(): Promise<void>
{
const [collections] = await getCollections();
const lines: string[] = [
"<!DOCTYPE NETSCAPE-Bookmark-file-1>",
"<!-- This is an automatically generated file.",
" It will be read and overwritten.",
" DO NOT EDIT! -->",
"<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">",
"<TITLE>Bookmarks</TITLE>",
"<H1>Bookmarks</H1>",
"<DL><p>"
];
for (const collection of collections)
lines.push(...createFolder(collection));
lines.push("</DL><p>");
const data: string = lines.join("\n");
const blob: Blob = new Blob([data], { type: "text/html" });
const element: HTMLAnchorElement = document.createElement("a");
element.style.display = "none";
element.href = URL.createObjectURL(blob);
element.setAttribute("download", "collections.html");
document.body.appendChild(element);
element.click();
URL.revokeObjectURL(element.href);
document.body.removeChild(element);
}
function createFolder(item: CollectionItem | GroupItem): string[]
{
const lines: string[] = [];
const title: string = item.type === "collection" ?
(item.title ?? getCollectionTitle(item)) :
(item.pinned ? i18n.t("groups.pinned") : (item.title ?? ""));
lines.push(`<DT><H3>${sanitizeString(title)}</H3>`);
lines.push("<DL><p>");
for (const subItem of item.items)
{
if (subItem.type === "tab")
lines.push(`<DT><A HREF="${encodeURI(subItem.url).replace(/"/g, "%22")}">${sanitizeString(subItem.title || subItem.url)}</A>`);
else if (subItem.type === "group")
lines.push(...createFolder(subItem));
}
lines.push("</DL><p>");
return lines;
}
function sanitizeString(str: string): string
{
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
@@ -0,0 +1,52 @@
import { getCollections, saveCollections } from "@/features/collectionStorage";
import { sendMessage } from "@/utils/messaging";
import parse from "node-bookmarks-parser";
import { Bookmark } from "node-bookmarks-parser/build/interfaces/bookmark";
import convertBookmarks from "./convertBookmarks";
export default async function importBookmarks(): Promise<number | null>
{
const element: HTMLInputElement = document.createElement("input");
element.style.display = "none";
element.hidden = true;
element.type = "file";
element.accept = ".html";
document.body.appendChild(element);
element.click();
await new Promise(resolve =>
{
const listener = () =>
{
element.removeEventListener("input", listener);
resolve(null);
};
element.addEventListener("input", listener);
});
if (!element.files || element.files.length < 1)
return null;
const file: File = element.files[0];
const content: string = await file.text();
document.body.removeChild(element);
try
{
const bookmarks: Bookmark[] = parse(content);
const [data, graphics, tabCount] = convertBookmarks(bookmarks);
const [collections, cloudIssues] = await getCollections();
await saveCollections([...data, ...collections], cloudIssues === null, graphics);
sendMessage("refreshCollections", undefined);
return tabCount;
}
catch (error)
{
console.error("Failed to parse bookmarks file", error);
return -1;
}
}
@@ -1,11 +1,11 @@
import { githubLinks } from "@/data/links"; import { githubLinks } from "@/data/links";
import { analyticsPermission } from "@/features/analytics"; import { analyticsPermission } from "@/features/analytics";
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
import extLink from "@/utils/extLink"; import extLink from "@/utils/extLink";
import * as fui from "@fluentui/react-components"; import * as fui from "@fluentui/react-components";
import { settingsForReview } from "../utils/showSettingsReviewDialog"; import { Unwatch } from "wxt/utils/storage";
import { reviewSettings } from "../utils/setSettingsReviewNeeded"; import { reviewSettings } from "../utils/setSettingsReviewNeeded";
import { Unwatch } from "wxt/storage"; import { settingsForReview } from "../utils/showSettingsReviewDialog";
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
export default function SettingsReviewDialog(): React.ReactElement export default function SettingsReviewDialog(): React.ReactElement
{ {
@@ -1,8 +1,7 @@
import { analyticsPermission } from "@/features/analytics"; import { analyticsPermission } from "@/features/analytics";
import { Runtime } from "wxt/browser";
import { settingsForReview } from "./showSettingsReviewDialog"; 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(); const needsReview: string[] = await settingsForReview.getValue();
@@ -25,7 +24,7 @@ export const reviewSettings =
THUMBNAILS: "thumbnails" 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") if (installReason === "install")
return !await analyticsPermission.getValue(); return !await analyticsPermission.getValue();
@@ -45,7 +44,7 @@ async function checkAnalyticsReviewNeeded(installReason: Runtime.OnInstalledReas
return false; 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") if (installReason === "install")
return true; return true;
+1 -1
View File
@@ -1,6 +1,6 @@
import { makeStyles, tokens } from "@fluentui/react-components"; 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: blue:
{ {
"--border": tokens.colorPaletteBlueBorderActive, "--border": tokens.colorPaletteBlueBorderActive,
+2 -2
View File
@@ -14,8 +14,8 @@ export default function useStorageInfo(): StorageInfoHook
return { return {
bytesInUse, bytesInUse,
storageQuota: chrome.storage.sync.QUOTA_BYTES ?? 102400, storageQuota: browser.storage.sync.QUOTA_BYTES ?? 102400,
usedStorageRatio: bytesInUse / (chrome.storage.sync.QUOTA_BYTES ?? 102400) usedStorageRatio: bytesInUse / (browser.storage.sync.QUOTA_BYTES ?? 102400)
}; };
} }
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "See the full list of what we collect" p3_text: "See the full list of what we collect"
p3_link: "here" p3_link: "here"
netscape_bookmarks:
title: "Browser bookmarks"
export: "Export collections as bookmarks"
import: "Import from bookmarks file"
import_dialog:
title: "Import bookmarks"
content: "Import bookmarks from a Netscape-format bookmarks file exported from your browser."
import_result:
success: "Successfully imported $1 bookmarks."
error: "Failed to import bookmarks. Please ensure the file is a valid bookmarks file."
notifications: notifications:
tabs_saved: tabs_saved:
title: "New collection created" title: "New collection created"
@@ -114,6 +125,8 @@ options_page:
restore: "Open tabs and remove the collection" restore: "Open tabs and remove the collection"
storage: storage:
title: "Storage" title: "Storage"
manage_title: "Storage management"
thumbnails_title: "Thumbnails & icons"
capacity: capacity:
title: "Cloud storage capacity" title: "Cloud storage capacity"
description: "$1 of $2 KiB" description: "$1 of $2 KiB"
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "Ver la lista completa de lo que recopilamos" p3_text: "Ver la lista completa de lo que recopilamos"
p3_link: "aquí" p3_link: "aquí"
netscape_bookmarks:
title: "Marcadores del navegador"
export: "Exportar colecciones como marcadores"
import: "Importar desde archivo de marcadores"
import_dialog:
title: "Importar marcadores"
content: "Importa marcadores desde un archivo de marcadores en formato Netscape exportado desde tu navegador."
import_result:
success: "Se importaron correctamente $1 marcadores."
error: "No se pudieron importar los marcadores. Asegúrate de que el archivo sea un archivo de marcadores válido."
notifications: notifications:
tabs_saved: tabs_saved:
title: "Nueva colección creada" title: "Nueva colección creada"
@@ -114,6 +125,8 @@ options_page:
restore: "Abrir pestañas y eliminar la colección" restore: "Abrir pestañas y eliminar la colección"
storage: storage:
title: "Almacenamiento" title: "Almacenamiento"
manage_title: "Administrar almacenamiento"
thumbnails_title: "Miniaturas e íconos"
capacity: capacity:
title: "Capacidad de almacenamiento en la nube" title: "Capacidad de almacenamiento en la nube"
description: "$1 de $2 KiB" description: "$1 de $2 KiB"
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "Vedi l'elenco completo di ciò che raccogliamo" p3_text: "Vedi l'elenco completo di ciò che raccogliamo"
p3_link: "qui" p3_link: "qui"
netscape_bookmarks:
title: "Segnalibri del browser"
export: "Esporta collezioni come segnalibri"
import: "Importa da file di segnalibri"
import_dialog:
title: "Importa segnalibri"
content: "Importa segnalibri da un file di segnalibri in formato Netscape esportato dal tuo browser."
import_result:
success: "Importati con successo $1 segnalibri."
error: "Impossibile importare i segnalibri. Assicurati che il file sia un file di segnalibri valido."
notifications: notifications:
tabs_saved: tabs_saved:
title: "Nuova collezione creata" title: "Nuova collezione creata"
@@ -114,6 +125,8 @@ options_page:
restore: "Apri le schede e rimuovi la collezione" restore: "Apri le schede e rimuovi la collezione"
storage: storage:
title: "Archiviazione" title: "Archiviazione"
manage_title: "Gestisci archiviazione"
thumbnails_title: "Miniature e icone"
capacity: capacity:
title: "Capacità di archiviazione cloud" title: "Capacità di archiviazione cloud"
description: "$1 di $2 KiB" description: "$1 di $2 KiB"
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "Pełną listę zbieranych danych można zobaczyć" p3_text: "Pełną listę zbieranych danych można zobaczyć"
p3_link: "tutaj" p3_link: "tutaj"
netscape_bookmarks:
title: "Import/eksport zakładek"
export: "Eksportuj kolekcje jako plik zakładek"
import: "Importuj z pliku zakładek"
import_dialog:
title: "Import zakładek"
content: "Importuj zakładki z pliku zakładek w formacie Netscape wyeksportowanego z przeglądarki."
import_result:
success: "Zakładki zostały pomyślnie zaimportowane ($1)"
error: "Nie udało się zaimportować zakładek. Upewnij się, że plik jest poprawnym plikiem zakładek."
notifications: notifications:
tabs_saved: tabs_saved:
title: "Utworzono nową kolekcję" title: "Utworzono nową kolekcję"
@@ -114,6 +125,8 @@ options_page:
restore: "Otwórz karty i usuń kolekcję" restore: "Otwórz karty i usuń kolekcję"
storage: storage:
title: "Magazyn" title: "Magazyn"
manage_title: "Zarządzaj magazynem"
thumbnails_title: "Podglądy i ikony"
capacity: capacity:
title: "Magazyn w chmurze" title: "Magazyn w chmurze"
description: "$1 z $2 KiB" description: "$1 z $2 KiB"
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "Veja a lista completa do que coletamos" p3_text: "Veja a lista completa do que coletamos"
p3_link: "aqui" p3_link: "aqui"
netscape_bookmarks:
title: "Favoritos do navegador"
export: "Exportar coleções como favoritos"
import: "Importar de arquivo de favoritos"
import_dialog:
title: "Importar favoritos"
content: "Importe favoritos de um arquivo de favoritos no formato Netscape exportado do seu navegador."
import_result:
success: "Importados com sucesso $1 favoritos."
error: "Falha ao importar favoritos. Por favor, certifique-se de que o arquivo é um arquivo de favoritos válido."
notifications: notifications:
tabs_saved: tabs_saved:
title: "Nova coleção criada" title: "Nova coleção criada"
@@ -114,6 +125,8 @@ options_page:
restore: "Abrir abas e remover a coleção" restore: "Abrir abas e remover a coleção"
storage: storage:
title: "Armazenamento" title: "Armazenamento"
manage_title: "Gerenciar armazenamento"
thumbnails_title: "Miniaturas e ícones"
capacity: capacity:
title: "Capacidade de armazenamento na nuvem" title: "Capacidade de armazenamento na nuvem"
description: "$1 de $2 KiB" description: "$1 de $2 KiB"
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "Полный список собираемых данных можно посмотреть" p3_text: "Полный список собираемых данных можно посмотреть"
p3_link: "здесь" p3_link: "здесь"
netscape_bookmarks:
title: "Импорт/экспорт закладок"
export: "Экспортировать коллекции как файл закладок"
import: "Импорт из файла закладок"
import_dialog:
title: "Импорт закладок"
content: "Импортируйте закладки из файла закладок в формате Netscape, экспортированного из вашего браузера."
import_result:
success: "Закладки успешно импортированы ($1 шт.)"
error: "Не удалось импортировать закладки. Пожалуйста, убедитесь, что файл является допустимым файлом закладок."
notifications: notifications:
tabs_saved: tabs_saved:
title: "Создана новая коллекция" title: "Создана новая коллекция"
@@ -114,6 +125,8 @@ options_page:
restore: "Открыть вкладки и удалить коллекцию" restore: "Открыть вкладки и удалить коллекцию"
storage: storage:
title: "Хранилище" title: "Хранилище"
manage_title: "Управление хранилищем"
thumbnails_title: "Превью и иконки"
capacity: capacity:
title: "Объём облачного хранилища" title: "Объём облачного хранилища"
description: "$1 из $2 КиБ" description: "$1 из $2 КиБ"
+16 -3
View File
@@ -46,6 +46,17 @@ features:
p3_text: "Повний список зібраних даних можна подивитися" p3_text: "Повний список зібраних даних можна подивитися"
p3_link: "тут" p3_link: "тут"
netscape_bookmarks:
title: "Імпорт/експорт закладок"
export: "Експортувати колекції як файл закладок"
import: "Імпорт із файлу закладок"
import_dialog:
title: "Імпорт закладок"
content: "Імпортуйте закладки з файлу закладок у форматі Netscape, експортованого з вашого браузера."
import_result:
success: "Закладки успішно імпортовані ($1 шт.)"
error: "Не вдалося імпортувати закладки. Будь ласка, переконайтеся, що файл є коректним файлом закладок."
notifications: notifications:
tabs_saved: tabs_saved:
title: "Створено нову колекцію" title: "Створено нову колекцію"
@@ -114,6 +125,8 @@ options_page:
restore: "Відкрити вкладки та видалити колекцію" restore: "Відкрити вкладки та видалити колекцію"
storage: storage:
title: "Сховище" title: "Сховище"
manage_title: "Керування сховищем"
thumbnails_title: "Прев'ю та іконки"
capacity: capacity:
title: "Хмарне сховище" title: "Хмарне сховище"
description: "$1 з $2 КіБ" description: "$1 з $2 КіБ"
@@ -132,13 +145,13 @@ options_page:
disable_prompt: disable_prompt:
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі." text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
action: "Вимкнути та перезавантажити розширення" action: "Вимкнути та перезавантажити розширення"
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок" thumbnail_capture: "Зберігати прев'ю і іконки для збережених вкладок"
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів" thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок" thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
clear_thumbnails: clear_thumbnails:
action: "Видалити збережені іконки" action: "Видалити збережені іконки"
title: "Видалити превью і іконки?" title: "Видалити прев'ю і іконки?"
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати." prompt: "Ця дія видалить всі прев'ю і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
about: about:
title: "О розширенні" title: "О розширенні"
developed_by: "Розробник: Євген Лис" developed_by: "Розробник: Євген Лис"
+13
View File
@@ -46,6 +46,17 @@ features:
p3_text: "请参阅我们收集内容的" p3_text: "请参阅我们收集内容的"
p3_link: "完整列表" p3_link: "完整列表"
netscape_bookmarks:
title: "浏览器书签"
export: "将收藏导出为书签"
import: "从书签文件导入"
import_dialog:
title: "导入书签"
content: "从您的浏览器导出的 Netscape 格式书签文件中导入书签。"
import_result:
success: "成功导入 $1 个书签。"
error: "导入书签失败。请确保该文件是有效的书签文件。"
notifications: notifications:
tabs_saved: tabs_saved:
title: "已创建新收藏" title: "已创建新收藏"
@@ -114,6 +125,8 @@ options_page:
restore: "打开标签页并删除收藏" restore: "打开标签页并删除收藏"
storage: storage:
title: "存储" title: "存储"
manage_title: "存储管理"
thumbnails_title: "缩略图和图标"
capacity: capacity:
title: "云存储容量" title: "云存储容量"
description: "$1 / $2 KiB" description: "$1 / $2 KiB"
+2 -2
View File
@@ -17,7 +17,7 @@ export type DefaultGroupItem =
type: "group"; type: "group";
pinned?: false; pinned?: false;
title?: string; title?: string;
color: chrome.tabGroups.ColorEnum; color: `${Browser.tabGroups.Color}`;
items: TabItem[]; items: TabItem[];
}; };
@@ -28,7 +28,7 @@ export type CollectionItem =
type: "collection"; type: "collection";
timestamp: number; timestamp: number;
title?: string; title?: string;
color?: chrome.tabGroups.ColorEnum; color?: `${Browser.tabGroups.Color}`;
items: (TabItem | GroupItem)[]; items: (TabItem | GroupItem)[];
}; };
+11095
View File
File diff suppressed because it is too large Load Diff
+15 -18
View File
@@ -1,46 +1,43 @@
{ {
"name": "tabs-aside", "name": "tabs-aside",
"private": true, "private": true,
"version": "3.2.0", "version": "3.2.3",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
"build": "yarn lint && wxt build --mv3", "build": "npm run lint && wxt build --mv3",
"zip": "yarn lint && wxt zip --mv3", "zip": "npm run lint && wxt zip --mv3",
"lint": "tsc --noEmit && eslint . -c eslint.config.js", "lint": "tsc --noEmit && eslint . -c eslint.config.js",
"prepare": "wxt prepare", "prepare": "wxt prepare",
"postinstall": "yarn prepare" "postinstall": "wxt prepare"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@fluentui/react-components": "^9.72.6", "@fluentui/react-components": "^9.72.8",
"@fluentui/react-icons": "^2.0.313", "@fluentui/react-icons": "^2.0.316",
"@webext-core/messaging": "^2.3.0", "@webext-core/messaging": "^2.3.0",
"@wxt-dev/analytics": "^0.5.1", "@wxt-dev/analytics": "^0.5.1",
"@wxt-dev/i18n": "^0.2.4", "@wxt-dev/i18n": "^0.2.4",
"lzutf8": "^0.6.3", "lzutf8": "^0.6.3",
"react": "~19.2.0", "node-bookmarks-parser": "^2.0.0",
"react-dom": "~19.2.0" "react": "^19.2.1",
"react-dom": "^19.2.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/css": "^0.14.1", "@eslint/css": "^0.14.1",
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@eslint/json": "^0.14.0", "@eslint/json": "^0.14.0",
"@stylistic/eslint-plugin": "^5.5.0", "@stylistic/eslint-plugin": "^5.6.1",
"@types/react": "~19.2.2", "@types/react": "^19.2.7",
"@types/react-dom": "~19.2.2", "@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.5", "@wxt-dev/module-react": "^1.1.5",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.5.0",
"scheduler": "0.23.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.46.4", "typescript-eslint": "^8.49.0",
"vite": "^7.2.2", "wxt": "^0.20.11"
"wxt": "~0.19.29" }
},
"packageManager": "yarn@4.9.2"
} }
+5 -5
View File
@@ -1,9 +1,9 @@
{ {
"extends": "./.wxt/tsconfig.json", "extends": "./.wxt/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"strict": true, "strict": true,
"strictNullChecks": true "strictNullChecks": true
} }
} }
+1 -3
View File
@@ -1,6 +1,4 @@
import { Tabs } from "wxt/browser"; export async function closeTabsAsync(tabs: Browser.tabs.Tab[]): Promise<void>
export async function closeTabsAsync(tabs: Tabs.Tab[]): Promise<void>
{ {
if (tabs.length < 1) if (tabs.length < 1)
return; return;
+3 -4
View File
@@ -1,7 +1,6 @@
import { CollectionItem, GroupItem } from "@/models/CollectionModels"; import { CollectionItem, GroupItem } from "@/models/CollectionModels";
import { Tabs } from "wxt/browser";
export async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<CollectionItem> export async function createCollectionFromTabs(tabs: Browser.tabs.Tab[]): Promise<CollectionItem>
{ {
const collection: CollectionItem = { const collection: CollectionItem = {
type: "collection", type: "collection",
@@ -36,7 +35,7 @@ export async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<Collec
tabs.every(i => i.groupId === tabs[0].groupId) tabs.every(i => i.groupId === tabs[0].groupId)
) )
{ {
const group = await chrome.tabGroups.get(tabs[0].groupId); const group = await browser.tabGroups.get(tabs[0].groupId);
collection.title = group.title; collection.title = group.title;
collection.color = group.color; collection.color = group.color;
@@ -63,7 +62,7 @@ export async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<Collec
if (!activeGroup || activeGroup !== tab.groupId) if (!activeGroup || activeGroup !== tab.groupId)
{ {
activeGroup = tab.groupId; activeGroup = tab.groupId;
const group = await chrome.tabGroups.get(activeGroup); const group = await browser.tabGroups.get(activeGroup!);
collection.items.push({ collection.items.push({
type: "group", type: "group",
+2 -3
View File
@@ -1,9 +1,8 @@
import { Tabs } from "wxt/browser";
import { settings } from "./settings"; import { settings } from "./settings";
export async function getTabsToSaveAsync(): Promise<[Tabs.Tab[], number]> export async function getTabsToSaveAsync(): Promise<[Browser.tabs.Tab[], number]>
{ {
let tabs: Tabs.Tab[] = await browser.tabs.query({ let tabs: Browser.tabs.Tab[] = await browser.tabs.query({
currentWindow: true, currentWindow: true,
highlighted: true highlighted: true
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
import { Unwatch } from "wxt/storage"; import { Unwatch } from "wxt/utils/storage";
export default function watchTabSelection(onChange: TabSelectChangeHandler): Unwatch export default function watchTabSelection(onChange: TabSelectChangeHandler): Unwatch
{ {
-9523
View File
File diff suppressed because it is too large Load Diff