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 8d9864b276 Patch 3.3.1 (#232)
* fix: opening tab context menu causes tab to open on Firefox #224 (#225)

* build(deps): dependency bump + audit fix + manifest version update

* chore(ci): dependabot group policy update

* docs: copyright dates

* chore(ui): minor branding update
2026-05-17 21:03:44 +12:00
xfox111 58d8e864e0 feat: Minor 3.3.0 (#222)
* feat: add ability to hide collections #211 (#213)

* feat: add ability to hide collections #211

* fix: hide/unhide collection label is swapped

* fix: missing useCallback dependency

* fix: add selected tabs to existing collection adds all tabs in current window #215 (#216)

* fix: add selected tabs to existing collection adds all tabs in current window #215

* fix: force selected tabs only for adding tabs to groups

* feat: compact collection view (#214)

* feat: compact collection view #201

* loc: compact view localization

* fix(loc): missing "color" translation in edit dialog

* feat: add ability to edit saved tabs (#218)

* feat: adds option to edit saved tabs #217

* loc: translations for #217

* build(deps): Bump the react group with 2 updates (#221)

Bumps the react group with 2 updates: [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.1 to 19.2.3
- [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.3/packages/react)

Updates `react-dom` from 19.2.1 to 19.2.3
- [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.3/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react
- dependency-name: react-dom
  dependency-version: 19.2.3
  dependency-type: direct:production
  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>

* build(deps): Bump the deps group with 4 updates (#220)

Bumps the deps group with 4 updates: [@fluentui/react-components](https://github.com/microsoft/fluentui), [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js), [eslint](https://github.com/eslint/eslint) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


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

Updates `@eslint/js` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/commits/v9.39.2/packages/js)

Updates `eslint` from 9.39.1 to 9.39.2
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

Updates `typescript-eslint` from 8.49.0 to 8.51.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.51.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@fluentui/react-components"
  dependency-version: 9.72.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: "@eslint/js"
  dependency-version: 9.39.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: typescript-eslint
  dependency-version: 8.51.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
...

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

* chore: Bump version from 3.2.3 to 3.3.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-03 21:13:08 +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
61 changed files with 11423 additions and 9850 deletions
+1 -1
View File
@@ -22,5 +22,5 @@
} }
}, },
"postCreateCommand": "yarn install" "postCreateCommand": "npm install"
} }
+8 -2
View File
@@ -52,7 +52,10 @@ updates:
schedule: schedule:
interval: monthly interval: monthly
rebase-strategy: disabled rebase-strategy: disabled
open-pull-requests-limit: 20 groups:
actions:
patterns:
- "*"
- package-ecosystem: "devcontainers" - package-ecosystem: "devcontainers"
directory: "/" directory: "/"
@@ -62,4 +65,7 @@ updates:
schedule: schedule:
interval: monthly interval: monthly
rebase-strategy: disabled rebase-strategy: disabled
open-pull-requests-limit: 20 groups:
devcontainers:
patterns:
- "*"
+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
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Eugene Fox Copyright (c) 2026 Eugene Fox
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -97,4 +97,4 @@ If you are interested in fixing issues and contributing directly to the code bas
[![GitHub](https://img.shields.io/badge/%40xfox111-GitHub?logo=github&logoColor=%23181717&label=GitHub&labelColor=white&color=%23181717)](https://github.com/xfox111) [![GitHub](https://img.shields.io/badge/%40xfox111-GitHub?logo=github&logoColor=%23181717&label=GitHub&labelColor=white&color=%23181717)](https://github.com/xfox111)
[![Buy Me a Coffee](https://img.shields.io/badge/%40xfox111-BMC?logo=buymeacoffee&logoColor=black&label=Buy%20me%20a%20coffee&labelColor=white&color=%23FFDD00)](https://buymeacoffee.com/xfox111) [![Buy Me a Coffee](https://img.shields.io/badge/%40xfox111-BMC?logo=buymeacoffee&logoColor=black&label=Buy%20me%20a%20coffee&labelColor=white&color=%23FFDD00)](https://buymeacoffee.com/xfox111)
> ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/TabsAsideExtension/blob/main/LICENSE) > ©2026 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/TabsAsideExtension/blob/main/LICENSE)
+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,11 @@ export const useOptionsStyles = makeStyles({
flexFlow: "column", flexFlow: "column",
alignItems: "flex-start", alignItems: "flex-start",
gap: tokens.spacingVerticalSNudge gap: tokens.spacingVerticalSNudge
},
img:
{
height: "100px",
flexGrow: 1,
alignSelf: "flex-end"
} }
}); });
+22 -20
View File
@@ -2,7 +2,7 @@ import { BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
import { bskyLink, buyMeACoffeeLink, githubLinks, storeLink, websiteLink } from "@/data/links"; import { bskyLink, buyMeACoffeeLink, githubLinks, storeLink, websiteLink } from "@/data/links";
import { useBmcStyles } from "@/hooks/useBmcStyles"; import { useBmcStyles } from "@/hooks/useBmcStyles";
import extLink from "@/utils/extLink"; import extLink from "@/utils/extLink";
import { Body1, Button, Caption1, Link, Subtitle1, Text } from "@fluentui/react-components"; import { Body1, Button, Caption1, Image, Link, Subtitle1, Text } from "@fluentui/react-components";
import { PersonFeedback20Regular } from "@fluentui/react-icons"; import { PersonFeedback20Regular } from "@fluentui/react-icons";
import { useOptionsStyles } from "../hooks/useOptionsStyles"; import { useOptionsStyles } from "../hooks/useOptionsStyles";
import Package from "@/package.json"; import Package from "@/package.json";
@@ -19,25 +19,6 @@ export default function AboutSection(): React.ReactElement
<sup><Caption1> v{ Package.version }</Caption1></sup> <sup><Caption1> v{ Package.version }</Caption1></sup>
</Text> </Text>
<Body1 as="p">
{ i18n.t("options_page.about.developed_by") } (<Link { ...extLink(bskyLink) }>@xfox111.net</Link>)<br />
{ i18n.t("options_page.about.licensed_under") } <Link { ...extLink(githubLinks.license) }>{ i18n.t("options_page.about.mit_license") }</Link>
</Body1>
<Body1 as="p">
{ i18n.t("options_page.about.translation_cta.text") }<br />
<Link { ...extLink(githubLinks.translationGuide) }>
{ i18n.t("options_page.about.translation_cta.button") }
</Link>
</Body1>
<Body1 as="p">
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
<Link { ...extLink(githubLinks.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link><br />
<Link { ...extLink(githubLinks.privacy) }>{ i18n.t("options_page.about.links.privacy") }</Link>
</Body1>
<div className={ cls.horizontalButtons }> <div className={ cls.horizontalButtons }>
<Button <Button
as="a" { ...extLink(storeLink) } as="a" { ...extLink(storeLink) }
@@ -54,6 +35,27 @@ export default function AboutSection(): React.ReactElement
{ i18n.t("common.cta.sponsor") } { i18n.t("common.cta.sponsor") }
</Button> </Button>
</div> </div>
<Body1 as="p">
{ i18n.t("options_page.about.translation_cta.text") }<br />
<Link { ...extLink(githubLinks.translationGuide) }>
{ i18n.t("options_page.about.translation_cta.button") }
</Link>
</Body1>
<Body1 as="p">
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
<Link { ...extLink(githubLinks.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link><br />
<Link { ...extLink(githubLinks.privacy) }>{ i18n.t("options_page.about.links.privacy") }</Link>
</Body1>
<Body1 as="p">
{ i18n.t("options_page.about.developed_by") } (<Link { ...extLink(bskyLink) }>@xfox111.net</Link>)<br />
{ i18n.t("options_page.about.licensed_under") } <Link { ...extLink(githubLinks.license) }>{ i18n.t("options_page.about.mit_license") }</Link>
</Body1>
<Image className={ cls.img } src="/fox.svg" />
</> </>
); );
} }
@@ -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);
@@ -4,10 +4,10 @@ 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, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, 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";
export default function StorageSection(): React.ReactElement export default function StorageSection(): React.ReactElement
{ {
@@ -19,6 +19,11 @@ export const useStyles_CollectionView = makeStyles({
"&:hover": "&:hover":
{ {
boxShadow: tokens.shadow4 boxShadow: tokens.shadow4
},
"&:not(:focus-within) .compact":
{
display: "none"
} }
}, },
color: color:
@@ -12,7 +12,12 @@ import { useStyles_CollectionView } from "./CollectionView.styles";
import GroupView from "./GroupView"; import GroupView from "./GroupView";
import TabView from "./TabView"; import TabView from "./TabView";
export default function CollectionView({ collection, index: collectionIndex, dragOverlay }: CollectionViewProps): ReactElement export default function CollectionView({
collection,
index: collectionIndex,
dragOverlay,
compact
}: CollectionViewProps): ReactElement
{ {
const { tilesView } = useCollections(); const { tilesView } = useCollections();
const { const {
@@ -53,12 +58,12 @@ export default function CollectionView({ collection, index: collectionIndex, dra
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay && { (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
<> <>
{ collection.items.length < 1 ? { collection.items.length < 1 ?
<div className={ cls.empty }> <div className={ mergeClasses(cls.empty, compact === true && "compact") }>
<CollectionsRegular fontSize={ 32 } /> <CollectionsRegular fontSize={ 32 } />
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong> <Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
</div> </div>
: :
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }> <div className={ mergeClasses(cls.list, !tilesView && cls.verticalList, compact === true && "compact") }>
<SortableContext <SortableContext
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) } items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy } strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
@@ -66,9 +71,12 @@ export default function CollectionView({ collection, index: collectionIndex, dra
{ collection.items.map((i, index) => { collection.items.map((i, index) =>
i.type === "group" ? i.type === "group" ?
<GroupView <GroupView
key={ index } group={ i } indices={ [collectionIndex, index] } /> key={ index } group={ i } indices={ [collectionIndex, index] }
collectionId={ collection.timestamp } />
: :
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } /> <TabView
key={ index } tab={ i } indices={ [collectionIndex, index] }
collectionId={ collection.timestamp } />
) } ) }
</SortableContext> </SortableContext>
</div> </div>
@@ -85,4 +93,5 @@ export type CollectionViewProps =
collection: CollectionItem; collection: CollectionItem;
index: number; index: number;
dragOverlay?: boolean; dragOverlay?: boolean;
compact?: boolean | null;
}; };
@@ -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")
@@ -87,7 +87,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
value={ color === "pinned" ? i18n.t("groups.pinned") : title } value={ color === "pinned" ? i18n.t("groups.pinned") : title }
onChange={ (_, e) => setTitle(e.value) } /> onChange={ (_, e) => setTitle(e.value) } />
</fui.Field> </fui.Field>
<fui.Field label="Color"> <fui.Field label={ i18n.t("dialogs.edit.color") }>
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }> <div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) && { (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
<fui.ToggleButton <fui.ToggleButton
@@ -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>
@@ -14,7 +14,7 @@ import GroupMoreMenu from "./collections/GroupMoreMenu";
import { useStyles_GroupView } from "./GroupView.styles"; import { useStyles_GroupView } from "./GroupView.styles";
import TabView from "./TabView"; import TabView from "./TabView";
export default function GroupView({ group, indices, dragOverlay }: GroupViewProps): ReactElement export default function GroupView({ group, indices, dragOverlay, collectionId }: GroupViewProps): ReactElement
{ {
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars"); const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
const { tilesView } = useCollections(); const { tilesView } = useCollections();
@@ -101,7 +101,9 @@ export default function GroupView({ group, indices, dragOverlay }: GroupViewProp
strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy } strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy }
> >
{ group.items.map((i, index) => { group.items.map((i, index) =>
<TabView key={ index } tab={ i } indices={ [...indices, index] } /> <TabView
key={ index } tab={ i } indices={ [...indices, index] }
collectionId={ collectionId } />
) } ) }
</SortableContext> </SortableContext>
</div> </div>
@@ -117,4 +119,5 @@ export type GroupViewProps =
group: GroupItem; group: GroupItem;
indices: number[]; indices: number[];
dragOverlay?: boolean; dragOverlay?: boolean;
collectionId: number;
}; };
@@ -0,0 +1,69 @@
import { track } from "@/features/analytics";
import { TabItem } from "@/models/CollectionModels";
import { Button, DialogActions, DialogBody, DialogContent, DialogSurface, DialogTitle, DialogTrigger, Field, Input, makeStyles, tokens } from "@fluentui/react-components";
export default function TabEditDialog({ tab, onSave }: TabEditDialogProps): React.ReactElement
{
const cls = useStyles();
const [title, setTitle] = useState(tab.title ?? "");
const [url, setUrl] = useState(tab.url);
const isValid = useMemo(() => url.trim().length > 0, [url]);
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
{
e.preventDefault();
track("item_edited", { type: "tab" });
onSave({
...tab,
title: title.trim().length > 0 ? title : undefined,
url: url.trim()
});
};
return (
<DialogSurface>
<form onSubmit={ onSubmit }>
<DialogBody>
<DialogTitle>{ i18n.t("dialogs.edit.title.edit_tab") }</DialogTitle>
<DialogContent className={ cls.content }>
<Input
value={ title } onChange={ (_, e) => setTitle(e.value) }
placeholder={ i18n.t("dialogs.edit.collection_title") } />
<Field validationMessage={ isValid ? undefined : i18n.t("dialogs.edit.url_error") }>
<Input
value={ url } onChange={ (_, e) => setUrl(e.value) }
placeholder="URL" />
</Field>
</DialogContent>
<DialogActions>
<DialogTrigger disableButtonEnhancement>
<Button disabled={ !isValid } appearance="primary" as="button" type="submit">
{ i18n.t("common.actions.save") }
</Button>
</DialogTrigger>
<DialogTrigger disableButtonEnhancement>
<Button appearance="subtle">{ i18n.t("common.actions.cancel") }</Button>
</DialogTrigger>
</DialogActions>
</DialogBody>
</form>
</DialogSurface>
);
}
const useStyles = makeStyles({
content:
{
display: "flex",
flexFlow: "column",
gap: tokens.spacingVerticalMNudge
}
});
export type TabEditDialogProps =
{
tab: TabItem;
onSave: (updatedTab: TabItem) => void;
};
@@ -0,0 +1,48 @@
import { useDangerStyles } from "@/hooks/useDangerStyles";
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import { bundleIcon, Delete20Filled, Delete20Regular, Edit20Filled, Edit20Regular, MoreHorizontal20Regular } from "@fluentui/react-icons";
import { ButtonHTMLAttributes } from "react";
export default function TabMoreButton({ onEdit, onDelete, ...props }: TabMoreButtonProps): React.ReactElement
{
const EditIcon = bundleIcon(Edit20Filled, Edit20Regular);
const DeleteIcon = bundleIcon(Delete20Filled, Delete20Regular);
const dangerCls = useDangerStyles();
const onClick = (ev: React.MouseEvent): void =>
{
ev.stopPropagation();
ev.preventDefault();
};
return (
<Menu>
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
<MenuTrigger disableButtonEnhancement>
<Button
appearance="subtle" icon={ <MoreHorizontal20Regular /> }
onClick={ onClick }
{ ...props } />
</MenuTrigger>
</Tooltip>
<MenuPopover onClick={ ev => ev.stopPropagation() }>
<MenuList>
<MenuItem icon={ <EditIcon /> } onClick={ onEdit }>
{ i18n.t("dialogs.edit.title.edit_tab") }
</MenuItem>
<MenuItem icon={ <DeleteIcon /> } className={ dangerCls.menuItem } onClick={ onDelete }>
{ i18n.t("tabs.delete") }
</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);
}
export type TabMoreButtonProps =
ButtonHTMLAttributes<HTMLButtonElement> &
{
onDelete?: () => void;
onEdit?: () => void;
};
+32 -15
View File
@@ -4,16 +4,17 @@ import { useDialog } from "@/contexts/DialogProvider";
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider"; import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem"; import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem";
import useSettings from "@/hooks/useSettings"; import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels"; import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
import { Button, Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components"; import { Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components";
import { Dismiss20Regular } from "@fluentui/react-icons";
import { MouseEventHandler, ReactElement } from "react"; import { MouseEventHandler, ReactElement } from "react";
import { useStyles_TabView } from "./TabView.styles"; import { useStyles_TabView } from "./TabView.styles";
import CollectionContext, { CollectionContextType } from "../contexts/CollectionContext"; import CollectionContext, { CollectionContextType } from "../contexts/CollectionContext";
import TabMoreButton from "./TabMoreButton";
import TabEditDialog from "./TabEditDialog";
export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement export default function TabView({ tab, indices, dragOverlay, collectionId }: TabViewProps): ReactElement
{ {
const { removeItem, graphics, tilesView } = useCollections(); const { removeItem, graphics, tilesView, collections, updateCollection } = useCollections();
const { collection } = useContext<CollectionContextType>(CollectionContext); const { collection } = useContext<CollectionContextType>(CollectionContext);
const { const {
setNodeRef, setActivatorNodeRef, setNodeRef, setActivatorNodeRef,
@@ -26,11 +27,8 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
const cls = useStyles_TabView(); const cls = useStyles_TabView();
const handleDelete: MouseEventHandler<HTMLButtonElement> = (args) => const handleDelete = (): void =>
{ {
args.preventDefault();
args.stopPropagation();
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)]; const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
if (deletePrompt) if (deletePrompt)
@@ -45,6 +43,26 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
removeItem(...removeIndex); removeItem(...removeIndex);
}; };
const handleEdit = (): void =>
{
if (collectionId < 0)
return;
const updateTab = async (updatedTab: TabItem): Promise<void> =>
{
const collection: CollectionItem = collections!.find(i => i.timestamp === collectionId)!;
if (indices.length > 2)
(collection.items[indices[1]] as GroupItem).items[indices[2]] = updatedTab;
else
collection.items[indices[1]] = updatedTab;
await updateCollection(collection, collection.timestamp);
};
dialog.pushCustom(<TabEditDialog tab={ tab } onSave={ updateTab } />);
};
const handleClick: MouseEventHandler<HTMLAnchorElement> = (args) => const handleClick: MouseEventHandler<HTMLAnchorElement> = (args) =>
{ {
args.preventDefault(); args.preventDefault();
@@ -91,12 +109,10 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
</Caption1> </Caption1>
</Tooltip> </Tooltip>
<Tooltip relationship="label" content={ i18n.t("tabs.delete") }> <TabMoreButton
<Button className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) }
className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) } onEdit={ handleEdit }
appearance="subtle" icon={ <Dismiss20Regular /> } onDelete={ handleDelete } />
onClick={ handleDelete } />
</Tooltip>
</div> </div>
</Link> </Link>
); );
@@ -107,4 +123,5 @@ export type TabViewProps =
tab: TabItem; tab: TabItem;
indices: number[]; indices: number[];
dragOverlay?: boolean; dragOverlay?: boolean;
collectionId: number;
}; };
@@ -2,7 +2,7 @@ import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionT
import useSettings from "@/hooks/useSettings"; import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels"; import { TabItem } from "@/models/CollectionModels";
import { Button, Caption1, makeStyles, mergeClasses, Subtitle2, tokens, Tooltip } from "@fluentui/react-components"; import { Button, Caption1, makeStyles, mergeClasses, Subtitle2, tokens, Tooltip } from "@fluentui/react-components";
import { Add20Filled, Add20Regular, bundleIcon } from "@fluentui/react-icons"; import { Add20Filled, Add20Regular, bundleIcon, EyeOff16Regular } from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext"; import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider"; import { useCollections } from "../../contexts/CollectionsProvider";
import CollectionMoreButton from "./CollectionMoreButton"; import CollectionMoreButton from "./CollectionMoreButton";
@@ -23,7 +23,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
const handleAddSelected = async () => const handleAddSelected = async () =>
{ {
const [newTabs, skipCount] = await getTabsToSaveAsync(); const [newTabs, skipCount] = await getTabsToSaveAsync(true);
if (newTabs.length > 0) if (newTabs.length > 0)
await updateCollection({ await updateCollection({
@@ -45,9 +45,12 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
content={ getCollectionTitle(collection) } content={ getCollectionTitle(collection) }
positioning="above-start" positioning="above-start"
> >
<Subtitle2 truncate wrap={ false } className={ cls.titleText }> <div className={ cls.titleContainer }>
{ getCollectionTitle(collection) } { collection.hidden && <EyeOff16Regular /> }
</Subtitle2> <Subtitle2 truncate wrap={ false } className={ cls.titleText }>
{ getCollectionTitle(collection) }
</Subtitle2>
</div>
</Tooltip> </Tooltip>
<Caption1> <Caption1>
@@ -112,5 +115,11 @@ const useStyles = makeStyles({
showToolbar: showToolbar:
{ {
display: "flex" display: "flex"
},
titleContainer:
{
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS
} }
}); });
@@ -22,6 +22,8 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
const EditIcon = ic.bundleIcon(ic.Edit20Filled, ic.Edit20Regular); const EditIcon = ic.bundleIcon(ic.Edit20Filled, ic.Edit20Regular);
const DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular); const DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular);
const BookmarkIcon = ic.bundleIcon(ic.BookmarkAdd20Filled, ic.BookmarkAdd20Regular); const BookmarkIcon = ic.bundleIcon(ic.BookmarkAdd20Filled, ic.BookmarkAdd20Regular);
const ShowIcon = ic.bundleIcon(ic.Eye20Filled, ic.Eye20Regular);
const HideIcon = ic.bundleIcon(ic.EyeOff20Filled, ic.EyeOff20Regular);
const dangerCls = useDangerStyles(); const dangerCls = useDangerStyles();
@@ -39,6 +41,11 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
removeItem(collection.timestamp); removeItem(collection.timestamp);
}; };
const toggleHidden = () =>
{
updateCollection({ ...collection, hidden: !collection.hidden }, collection.timestamp);
};
const handleEdit = () => const handleEdit = () =>
dialog.pushCustom( dialog.pushCustom(
<EditDialog <EditDialog
@@ -82,6 +89,9 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }> <MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
{ i18n.t("collections.menu.edit") } { i18n.t("collections.menu.edit") }
</MenuItem> </MenuItem>
<MenuItem icon={ collection.hidden ? <ShowIcon /> : <HideIcon /> } onClick={ toggleHidden }>
{ collection.hidden ? i18n.t("collections.menu.unhide") : i18n.t("collections.menu.hide") }
</MenuItem>
<MenuItem icon={ <DeleteIcon /> } className={ dangerCls.menuItem } onClick={ handleDelete }> <MenuItem icon={ <DeleteIcon /> } className={ dangerCls.menuItem } onClick={ handleDelete }>
{ i18n.t("collections.menu.delete") } { i18n.t("collections.menu.delete") }
</MenuItem> </MenuItem>
@@ -67,7 +67,7 @@ export default function GroupMoreMenu(): ReactElement
const handleAddSelected = async () => const handleAddSelected = async () =>
{ {
const [newTabs, skipCount] = await getTabsToSaveAsync(); const [newTabs, skipCount] = await getTabsToSaveAsync(true);
if (newTabs.length > 0) if (newTabs.length > 0)
await updateGroup({ await updateGroup({
@@ -51,5 +51,9 @@ export const useStyles_CollectionListView = makeStyles({
{ {
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))" gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
} }
},
compactList:
{
alignItems: "baseline"
} }
}); });
@@ -18,10 +18,10 @@ import CollectionContext from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider"; import { useCollections } from "../../contexts/CollectionsProvider";
import applyReorder from "../../utils/dnd/applyReorder"; import applyReorder from "../../utils/dnd/applyReorder";
import { collisionDetector } from "../../utils/dnd/collisionDetector"; import { collisionDetector } from "../../utils/dnd/collisionDetector";
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
import { useStyles_CollectionListView } from "./CollectionListView.styles"; import { useStyles_CollectionListView } from "./CollectionListView.styles";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage"; import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
export default function CollectionListView(): ReactElement export default function CollectionListView(): ReactElement
{ {
@@ -30,17 +30,19 @@ export default function CollectionListView(): ReactElement
const [sortMode, setSortMode] = useSettings("sortMode"); const [sortMode, setSortMode] = useSettings("sortMode");
const [query, setQuery] = useState<string>(""); const [query, setQuery] = useState<string>("");
const [colors, setColors] = useState<CollectionFilterType["colors"]>([]); const [colors, setColors] = useState<CollectionFilterType["colors"]>([]);
const [showHidden, setShowHidden] = useState<boolean>(false);
const [compactView] = useSettings("compactView");
const [active, setActive] = useState<DndItem | null>(null); const [active, setActive] = useState<DndItem | null>(null);
const sensors = useSensors( const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { delay: 10, tolerance: 20 } }), useSensor(MouseSensor, { activationConstraint: { delay: 150, tolerance: 20 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } }) useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } })
); );
const resultList = useMemo( const resultList = useMemo(
() => sortCollections(filterCollections(collections, { query, colors }), sortMode), () => sortCollections(filterCollections(collections, { query, colors, showHidden }), sortMode),
[query, colors, sortMode, collections] [query, colors, sortMode, collections, showHidden]
); );
const cls = useStyles_CollectionListView(); const cls = useStyles_CollectionListView();
@@ -49,6 +51,13 @@ export default function CollectionListView(): ReactElement
{ {
setQuery(""); setQuery("");
setColors([]); setColors([]);
setShowHidden(false);
}, []);
const updateFilter = useCallback((newColors: CollectionFilterType["colors"], newShowHidden: boolean) =>
{
setColors(newColors);
setShowHidden(newShowHidden);
}, []); }, []);
const handleDragStart = (event: DragStartEvent): void => const handleDragStart = (event: DragStartEvent): void =>
@@ -87,8 +96,9 @@ export default function CollectionListView(): ReactElement
<article className={ cls.root }> <article className={ cls.root }>
<SearchBar <SearchBar
query={ query } onQueryChange={ setQuery } query={ query } onQueryChange={ setQuery }
filter={ colors } onFilterChange={ setColors } filter={ colors } onFilterChange={ updateFilter }
sort={ sortMode } onSortChange={ setSortMode } sort={ sortMode } onSortChange={ setSortMode }
showHidden={ showHidden }
onReset={ resetFilter } /> onReset={ resetFilter } />
<CtaMessage className={ cls.msgBar } /> <CtaMessage className={ cls.msgBar } />
@@ -105,7 +115,7 @@ export default function CollectionListView(): ReactElement
</Button> </Button>
</div> </div>
: :
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView) }> <section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView, !!(!tilesView && compactView) && cls.compactList) }>
<DndContext <DndContext
sensors={ sensors } sensors={ sensors }
collisionDetection={ collisionDetector(!tilesView) } collisionDetection={ collisionDetector(!tilesView) }
@@ -118,7 +128,7 @@ export default function CollectionListView(): ReactElement
strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy } strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy }
> >
{ resultList.map((collection, index) => { resultList.map((collection, index) =>
<CollectionView key={ index } collection={ collection } index={ index } /> <CollectionView key={ index } collection={ collection } index={ index } compact={ compactView } />
) } ) }
</SortableContext> </SortableContext>
@@ -135,9 +145,9 @@ export default function CollectionListView(): ReactElement
} } } }
> >
{ active.item.type === "group" ? { active.item.type === "group" ?
<GroupView group={ active.item } indices={ [-1] } dragOverlay /> <GroupView group={ active.item } indices={ [-1] } collectionId={ -1 } dragOverlay />
: :
<TabView tab={ active.item } indices={ [-1] } dragOverlay /> <TabView tab={ active.item } indices={ [-1] } collectionId={ -1 } dragOverlay />
} }
</CollectionContext.Provider> </CollectionContext.Provider>
: :
@@ -3,32 +3,48 @@ import * as fui from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons"; import * as ic from "@fluentui/react-icons";
import { CollectionFilterType } from "../../utils/filterCollections"; import { CollectionFilterType } from "../../utils/filterCollections";
export default function FilterCollectionsButton({ value, onChange }: FilterCollectionsButtonProps): React.ReactElement export default function FilterCollectionsButton({ value, onChange, showHidden }: FilterCollectionsButtonProps): React.ReactElement
{ {
const cls = useStyles(); const cls = useStyles();
const colorCls = useGroupColors(); const colorCls = useGroupColors();
const ColorFilterIcon = ic.bundleIcon(ic.Color20Filled, ic.Color20Regular); const FilterIcon = ic.bundleIcon(ic.Filter20Filled, ic.Filter20Regular);
const ColorIcon = ic.bundleIcon(ic.Circle20Filled, ic.CircleHalfFill20Regular); const ColorIcon = ic.bundleIcon(ic.Circle20Filled, ic.CircleHalfFill20Regular);
const NoColorIcon = ic.bundleIcon(ic.CircleOffFilled, ic.CircleOffRegular); const NoColorIcon = ic.bundleIcon(ic.CircleOffFilled, ic.CircleOffRegular);
const AnyColorIcon = ic.bundleIcon(ic.PhotoFilter20Filled, ic.PhotoFilter20Regular); const AnyColorIcon = ic.bundleIcon(ic.PhotoFilter20Filled, ic.PhotoFilter20Regular);
const HiddenIcon = ic.bundleIcon(ic.EyeOff20Filled, ic.EyeOff20Regular);
const values: Record<string, string[]> = useMemo(() => ({
default: !value || value.length < 1 ? ["any"] : [],
colors: value || [],
hidden: showHidden ? ["show"] : []
}), [value, showHidden]);
const onCheckedValueChange = useCallback((_: fui.MenuCheckedValueChangeEvent, e: fui.MenuCheckedValueChangeData) =>
{
if (e.name === "hidden")
onChange?.(value ?? [], e.checkedItems.includes("show"));
else
onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"], showHidden ?? false);
}, [onChange, showHidden, value]);
return ( return (
<fui.Menu <fui.Menu
checkedValues={ !value || value.length < 1 ? { default: ["any"] } : { colors: value } } checkedValues={ values }
onCheckedValueChange={ (_, e) => onCheckedValueChange={ onCheckedValueChange }
onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"])
}
> >
<fui.Tooltip relationship="label" content={ i18n.t("main.list.searchbar.filter") }> <fui.Tooltip relationship="label" content={ i18n.t("main.list.searchbar.filter") }>
<fui.MenuTrigger disableButtonEnhancement> <fui.MenuTrigger disableButtonEnhancement>
<fui.Button appearance="subtle" icon={ <ColorFilterIcon /> } /> <fui.Button appearance="subtle" icon={ <FilterIcon /> } />
</fui.MenuTrigger> </fui.MenuTrigger>
</fui.Tooltip> </fui.Tooltip>
<fui.MenuPopover> <fui.MenuPopover>
<fui.MenuList> <fui.MenuList>
<fui.MenuItemCheckbox name="hidden" value="show" icon={ <HiddenIcon /> }>
{ i18n.t("main.list.searchbar.show_hidden") }
</fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="default" value="any" icon={ <AnyColorIcon /> }> <fui.MenuItemCheckbox name="default" value="any" icon={ <AnyColorIcon /> }>
{ i18n.t("colors.any") } { i18n.t("colors.any") }
</fui.MenuItemCheckbox> </fui.MenuItemCheckbox>
@@ -44,11 +60,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>
@@ -60,7 +76,8 @@ export default function FilterCollectionsButton({ value, onChange }: FilterColle
export type FilterCollectionsButtonProps = export type FilterCollectionsButtonProps =
{ {
value?: CollectionFilterType["colors"]; value?: CollectionFilterType["colors"];
onChange?: (value: CollectionFilterType["colors"]) => void; showHidden?: boolean;
onChange?: (value: CollectionFilterType["colors"], showHidden: boolean) => void;
}; };
const useStyles = fui.makeStyles({ const useStyles = fui.makeStyles({
@@ -25,7 +25,7 @@ export default function SearchBar(props: SearchBarProps): React.ReactElement
<Button appearance="subtle" icon={ <ResetIcon /> } onClick={ props.onReset } /> <Button appearance="subtle" icon={ <ResetIcon /> } onClick={ props.onReset } />
</Tooltip> </Tooltip>
} }
<FilterCollectionsButton value={ props.filter } onChange={ props.onFilterChange } /> <FilterCollectionsButton value={ props.filter } onChange={ props.onFilterChange } showHidden={ props.showHidden } />
<SortCollectionsButton value={ props.sort } onChange={ props.onSortChange } /> <SortCollectionsButton value={ props.sort } onChange={ props.onSortChange } />
</> </>
} /> } />
@@ -37,8 +37,9 @@ export type SearchBarProps =
query?: string; query?: string;
onQueryChange?: (query: string) => void; onQueryChange?: (query: string) => void;
filter?: CollectionFilterType["colors"]; filter?: CollectionFilterType["colors"];
onFilterChange?: (filter: CollectionFilterType["colors"]) => void; onFilterChange?: (filter: CollectionFilterType["colors"], showHidden: boolean) => void;
sort?: CollectionSortMode; sort?: CollectionSortMode;
showHidden?: boolean;
onSortChange?: (sort: CollectionSortMode) => void; onSortChange?: (sort: CollectionSortMode) => void;
onReset?: () => void; onReset?: () => void;
}; };
@@ -11,18 +11,32 @@ import { ReactElement } from "react";
export default function MoreButton(): ReactElement export default function MoreButton(): ReactElement
{ {
const [tilesView, setTilesView] = useSettings("tilesView"); const [tilesView, setTilesView] = useSettings("tilesView");
const [compactView, setCompactView] = useSettings("compactView");
const SettingsIcon: ic.FluentIcon = ic.bundleIcon(ic.Settings20Filled, ic.Settings20Regular); const SettingsIcon: ic.FluentIcon = ic.bundleIcon(ic.Settings20Filled, ic.Settings20Regular);
const ViewIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular); const GridIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular);
const CompactIcon: ic.FluentIcon = ic.bundleIcon(ic.ArrowMinimizeVerticalFilled, ic.ArrowMinimizeVerticalRegular);
const FeedbackIcon: ic.FluentIcon = ic.bundleIcon(ic.PersonFeedback20Filled, ic.PersonFeedback20Regular); const FeedbackIcon: ic.FluentIcon = ic.bundleIcon(ic.PersonFeedback20Filled, ic.PersonFeedback20Regular);
const LearnIcon: ic.FluentIcon = ic.bundleIcon(ic.QuestionCircle20Filled, ic.QuestionCircle20Regular); const LearnIcon: ic.FluentIcon = ic.bundleIcon(ic.QuestionCircle20Filled, ic.QuestionCircle20Regular);
const BmcIcon: ic.FluentIcon = ic.bundleIcon(BuyMeACoffee20Filled, BuyMeACoffee20Regular); const BmcIcon: ic.FluentIcon = ic.bundleIcon(BuyMeACoffee20Filled, BuyMeACoffee20Regular);
const checkedValues = useMemo(() => ({
view: [
tilesView ? "tiles" : "",
compactView ? "compact" : ""
]
}), [tilesView, compactView]);
const onCheckedValueChange = (_: unknown, e: fui.MenuCheckedValueChangeData) =>
{
setTilesView(e.checkedItems.includes("tiles"));
setCompactView(e.checkedItems.includes("compact"));
};
return ( return (
<fui.Menu <fui.Menu
hasIcons hasCheckmarks hasIcons hasCheckmarks
checkedValues={ { tilesView: tilesView ? ["true"] : [] } } checkedValues={ checkedValues } onCheckedValueChange={ onCheckedValueChange }
onCheckedValueChange={ (_, e) => setTilesView(e.checkedItems.length > 0) }
> >
<fui.Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }> <fui.Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
<fui.MenuTrigger disableButtonEnhancement> <fui.MenuTrigger disableButtonEnhancement>
@@ -36,9 +50,12 @@ export default function MoreButton(): ReactElement
<fui.MenuItem icon={ <SettingsIcon /> } onClick={ () => browser.runtime.openOptionsPage() }> <fui.MenuItem icon={ <SettingsIcon /> } onClick={ () => browser.runtime.openOptionsPage() }>
{ i18n.t("options_page.title") } { i18n.t("options_page.title") }
</fui.MenuItem> </fui.MenuItem>
<fui.MenuItemCheckbox name="tilesView" value="true" icon={ <ViewIcon /> }> <fui.MenuItemCheckbox name="view" value="tiles" icon={ <GridIcon /> }>
{ i18n.t("main.header.menu.tiles_view") } { i18n.t("main.header.menu.tiles_view") }
</fui.MenuItemCheckbox> </fui.MenuItemCheckbox>
<fui.MenuItemCheckbox name="view" value="compact" icon={ <CompactIcon /> }>
{ i18n.t("main.header.menu.compact_view") }
</fui.MenuItemCheckbox>
<fui.MenuDivider /> <fui.MenuDivider />
@@ -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)
}); });
@@ -9,13 +9,16 @@ export default function filterCollections(
if (!collections || collections.length < 1) if (!collections || collections.length < 1)
return []; return [];
if (!filter.query && filter.colors.length < 1) if (!filter.query && filter.colors.length < 1 && filter.showHidden)
return collections; return collections;
const query: string = filter.query.toLocaleLowerCase(); const query: string = filter.query.toLocaleLowerCase();
return collections.filter(collection => return collections.filter(collection =>
{ {
if (filter.showHidden === false && collection.hidden === true)
return false;
let querySatisfied: boolean = query.length < 1 || let querySatisfied: boolean = query.length < 1 ||
getCollectionTitle(collection).toLocaleLowerCase().includes(query); getCollectionTitle(collection).toLocaleLowerCase().includes(query);
let colorSatisfied: boolean = filter.colors.length < 1 || let colorSatisfied: boolean = filter.colors.length < 1 ||
@@ -61,5 +64,6 @@ export default function filterCollections(
export type CollectionFilterType = export type CollectionFilterType =
{ {
query: string; query: string;
colors: (chrome.tabGroups.ColorEnum | "none")[]; colors: (`${Browser.tabGroups.Color}` | "none")[];
showHidden: boolean;
}; };
@@ -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>"))
{ {
@@ -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)
}; };
} }
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Add empty group" add_group: "Add empty group"
export_bookmarks: "Export to bookmarks" export_bookmarks: "Export to bookmarks"
edit: "Edit collection" edit: "Edit collection"
hide: "Hide collection"
unhide: "Unhide collection"
groups: groups:
title: "Group" title: "Group"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Edit collection" edit_collection: "Edit collection"
edit_group: "Edit group" edit_group: "Edit group"
edit_tab: "Edit tab"
new_group: "New group" new_group: "New group"
new_collection: "New collection" new_collection: "New collection"
collection_title: "Title" collection_title: "Title"
color: "Color" color: "Color"
url_error: "URL is required"
main: main:
header: header:
create_collection: "Create new collection" create_collection: "Create new collection"
menu: menu:
tiles_view: "Tiles view" tiles_view: "Tiles view"
compact_view: "Compact view"
changelog: "What's new?" changelog: "What's new?"
list: list:
searchbar: searchbar:
title: "Search" title: "Search"
filter: "Filter" filter: "Filter"
show_hidden: "Show hidden"
sort: sort:
title: "Sort" title: "Sort"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Agregar grupo vacío" add_group: "Agregar grupo vacío"
export_bookmarks: "Exportar a marcadores" export_bookmarks: "Exportar a marcadores"
edit: "Editar colección" edit: "Editar colección"
hide: "Ocultar colección"
unhide: "Mostrar colección"
groups: groups:
title: "Grupo" title: "Grupo"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Editar colección" edit_collection: "Editar colección"
edit_group: "Editar grupo" edit_group: "Editar grupo"
edit_tab: "Editar pestaña"
new_group: "Nuevo grupo" new_group: "Nuevo grupo"
new_collection: "Nueva colección" new_collection: "Nueva colección"
collection_title: "Título" collection_title: "Título"
color: "Color" color: "Color"
url_error: "La URL es obligatoria"
main: main:
header: header:
create_collection: "Crear nueva colección" create_collection: "Crear nueva colección"
menu: menu:
tiles_view: "Vista de mosaicos" tiles_view: "Vista de mosaicos"
compact_view: "Vista compacta"
changelog: "¿Qué hay de nuevo?" changelog: "¿Qué hay de nuevo?"
list: list:
searchbar: searchbar:
title: "Buscar" title: "Buscar"
filter: "Filtrar" filter: "Filtrar"
show_hidden: "Mostrar ocultas"
sort: sort:
title: "Ordenar" title: "Ordenar"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Aggiungi gruppo vuoto" add_group: "Aggiungi gruppo vuoto"
export_bookmarks: "Esporta nei segnalibri" export_bookmarks: "Esporta nei segnalibri"
edit: "Modifica collezione" edit: "Modifica collezione"
hide: "Nascondi collezione"
unhide: "Mostra collezione"
groups: groups:
title: "Gruppo" title: "Gruppo"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Modifica collezione" edit_collection: "Modifica collezione"
edit_group: "Modifica gruppo" edit_group: "Modifica gruppo"
edit_tab: "Modifica scheda"
new_group: "Nuovo gruppo" new_group: "Nuovo gruppo"
new_collection: "Nuova collezione" new_collection: "Nuova collezione"
collection_title: "Titolo" collection_title: "Titolo"
color: "Colore" color: "Colore"
url_error: "L'URL è obbligatorio"
main: main:
header: header:
create_collection: "Crea nuova collezione" create_collection: "Crea nuova collezione"
menu: menu:
tiles_view: "Vista a riquadri" tiles_view: "Vista a riquadri"
compact_view: "Vista compatta"
changelog: "Cosa c'è di nuovo?" changelog: "Cosa c'è di nuovo?"
list: list:
searchbar: searchbar:
title: "Cerca" title: "Cerca"
filter: "Filtra" filter: "Filtra"
show_hidden: "Mostra nascoste"
sort: sort:
title: "Ordina" title: "Ordina"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Dodaj pustą grupę" add_group: "Dodaj pustą grupę"
export_bookmarks: "Eksportuj do zakładek" export_bookmarks: "Eksportuj do zakładek"
edit: "Edytuj kolekcję" edit: "Edytuj kolekcję"
hide: "Ukryj kolekcję"
unhide: "Pokaż kolekcję"
groups: groups:
title: "Grupa" title: "Grupa"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Edytuj kolekcję" edit_collection: "Edytuj kolekcję"
edit_group: "Edytuj grupę" edit_group: "Edytuj grupę"
edit_tab: "Edytuj zakładkę"
new_group: "Nowa grupa" new_group: "Nowa grupa"
new_collection: "Nowa kolekcja" new_collection: "Nowa kolekcja"
collection_title: "Nazwij" collection_title: "Nazwij"
color: "Kolor" color: "Kolor"
url_error: "URL jest wymagany"
main: main:
header: header:
create_collection: "Utwórz nową kolekcję" create_collection: "Utwórz nową kolekcję"
menu: menu:
tiles_view: "Kafelki" tiles_view: "Kafelki"
compact_view: "Widok kompaktowy"
changelog: "Co nowego?" changelog: "Co nowego?"
list: list:
searchbar: searchbar:
title: "Szukaj" title: "Szukaj"
filter: "Filtr" filter: "Filtr"
show_hidden: "Pokaż ukryte"
sort: sort:
title: "Sortowanie" title: "Sortowanie"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Adicionar grupo vazio" add_group: "Adicionar grupo vazio"
export_bookmarks: "Exportar para favoritos" export_bookmarks: "Exportar para favoritos"
edit: "Editar coleção" edit: "Editar coleção"
hide: "Ocultar coleção"
unhide: "Mostrar coleção"
groups: groups:
title: "Grupo" title: "Grupo"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Editar coleção" edit_collection: "Editar coleção"
edit_group: "Editar grupo" edit_group: "Editar grupo"
edit_tab: "Editar aba"
new_group: "Novo grupo" new_group: "Novo grupo"
new_collection: "Nova coleção" new_collection: "Nova coleção"
collection_title: "Título" collection_title: "Título"
color: "Cor" color: "Cor"
url_error: "A URL é obrigatória"
main: main:
header: header:
create_collection: "Criar nova coleção" create_collection: "Criar nova coleção"
menu: menu:
tiles_view: "Visualização em blocos" tiles_view: "Visualização em blocos"
compact_view: "Visualização compacta"
changelog: "O que há de novo?" changelog: "O que há de novo?"
list: list:
searchbar: searchbar:
title: "Pesquisar" title: "Pesquisar"
filter: "Filtrar" filter: "Filtrar"
show_hidden: "Mostrar ocultas"
sort: sort:
title: "Ordenar" title: "Ordenar"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Добавить пустую группу" add_group: "Добавить пустую группу"
export_bookmarks: "Экспортировать в закладки" export_bookmarks: "Экспортировать в закладки"
edit: "Редактировать коллекцию" edit: "Редактировать коллекцию"
hide: "Скрыть коллекцию"
unhide: "Показать коллекцию"
groups: groups:
title: "Группа" title: "Группа"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Редактировать коллекцию" edit_collection: "Редактировать коллекцию"
edit_group: "Редактировать группу" edit_group: "Редактировать группу"
edit_tab: "Редактировать вкладку"
new_group: "Новая группа" new_group: "Новая группа"
new_collection: "Новая коллекция" new_collection: "Новая коллекция"
collection_title: "Название" collection_title: "Название"
color: "Цвет" color: "Цвет"
url_error: "URL является обязательным"
main: main:
header: header:
create_collection: "Создать новую коллекцию" create_collection: "Создать новую коллекцию"
menu: menu:
tiles_view: "Плитки" tiles_view: "Плитки"
compact_view: "Компактный вид"
changelog: "Что нового?" changelog: "Что нового?"
list: list:
searchbar: searchbar:
title: "Поиск" title: "Поиск"
filter: "Фильтр" filter: "Фильтр"
show_hidden: "Показать скрытые"
sort: sort:
title: "Сортировка" title: "Сортировка"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "Додати порожню групу" add_group: "Додати порожню групу"
export_bookmarks: "Експортувати в закладки" export_bookmarks: "Експортувати в закладки"
edit: "Редагувати колекцію" edit: "Редагувати колекцію"
hide: "Приховати колекцію"
unhide: "Показати колекцію"
groups: groups:
title: "Група" title: "Група"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "Редагувати колекцію" edit_collection: "Редагувати колекцію"
edit_group: "Редагувати групу" edit_group: "Редагувати групу"
edit_tab: "Редагувати вкладку"
new_group: "Нова група" new_group: "Нова група"
new_collection: "Нова колекція" new_collection: "Нова колекція"
collection_title: "Назва" collection_title: "Назва"
color: "Колір" color: "Колір"
url_error: "URL є обов'язковим"
main: main:
header: header:
create_collection: "Створити нову колекцію" create_collection: "Створити нову колекцію"
menu: menu:
tiles_view: "Плитки" tiles_view: "Плитки"
compact_view: "Компактний вид"
changelog: "Що нового?" changelog: "Що нового?"
list: list:
searchbar: searchbar:
title: "Пошук" title: "Пошук"
filter: "Фільтр" filter: "Фільтр"
show_hidden: "Показати приховані"
sort: sort:
title: "Сортування" title: "Сортування"
options: options:
+6
View File
@@ -184,6 +184,8 @@ collections:
add_group: "添加空分组" add_group: "添加空分组"
export_bookmarks: "导出到书签" export_bookmarks: "导出到书签"
edit: "编辑收藏" edit: "编辑收藏"
hide: "隐藏收藏"
unhide: "显示收藏"
groups: groups:
title: "分组" title: "分组"
@@ -219,21 +221,25 @@ dialogs:
title: title:
edit_collection: "编辑收藏" edit_collection: "编辑收藏"
edit_group: "编辑分组" edit_group: "编辑分组"
edit_tab: "编辑标签页"
new_group: "新分组" new_group: "新分组"
new_collection: "新收藏" new_collection: "新收藏"
collection_title: "标题" collection_title: "标题"
color: "颜色" color: "颜色"
url_error: "需要 URL"
main: main:
header: header:
create_collection: "创建新收藏" create_collection: "创建新收藏"
menu: menu:
tiles_view: "平铺视图" tiles_view: "平铺视图"
compact_view: "紧凑视图"
changelog: "更新内容" changelog: "更新内容"
list: list:
searchbar: searchbar:
title: "搜索" title: "搜索"
filter: "筛选" filter: "筛选"
show_hidden: "显示隐藏项"
sort: sort:
title: "排序" title: "排序"
options: options:
+3 -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,8 +28,9 @@ 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)[];
hidden?: boolean;
}; };
export type GraphicsStorage = Record<string, GraphicsItem>; export type GraphicsStorage = Record<string, GraphicsItem>;
+10845
View File
File diff suppressed because it is too large Load Diff
+20 -24
View File
@@ -1,46 +1,42 @@
{ {
"name": "tabs-aside", "name": "tabs-aside",
"private": true, "private": true,
"version": "3.2.0", "version": "3.3.1",
"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.73.8",
"@fluentui/react-icons": "^2.0.313", "@fluentui/react-icons": "^2.0.326",
"@webext-core/messaging": "^2.3.0", "@webext-core/messaging": "^2.3.0",
"@wxt-dev/analytics": "^0.5.1", "@wxt-dev/analytics": "^0.5.4",
"@wxt-dev/i18n": "^0.2.4", "@wxt-dev/i18n": "^0.2.5",
"lzutf8": "^0.6.3", "lzutf8": "^0.6.3",
"react": "~19.2.0", "react": "^19.2.6",
"react-dom": "~19.2.0" "react-dom": "^19.2.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/css": "^0.14.1", "@eslint/css": "^0.14.1",
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.4",
"@eslint/json": "^0.14.0", "@eslint/json": "^0.14.0",
"@stylistic/eslint-plugin": "^5.5.0", "@stylistic/eslint-plugin": "^5.10.0",
"@types/react": "~19.2.2", "@types/react": "^19.2.14",
"@types/react-dom": "~19.2.2", "@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.5", "@wxt-dev/module-react": "^1.2.2",
"eslint": "^9.39.1", "eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.5.0", "typescript": "^6.0.3",
"scheduler": "0.23.0", "typescript-eslint": "^8.59.3",
"typescript": "^5.9.3", "wxt": "^0.20.26"
"typescript-eslint": "^8.46.4", }
"vite": "^7.2.2",
"wxt": "~0.19.29"
},
"packageManager": "yarn@4.9.2"
} }
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_5" data-name="Layer 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 1000">
<defs>
<style>
.cls-1,
.cls-2,
.cls-3 {
stroke-width: 0px;
}
.cls-1,
.cls-4 {
fill: #ff7545;
}
.cls-2,
.cls-6 {
fill: #242424;
}
.cls-5 {
stroke-width: 12px;
stroke-linejoin: round;
}
.cls-6,
.cls-4 {
stroke: #242424;
stroke-linejoin: round;
}
.cls-6,
.cls-4 {
stroke-linecap: round;
stroke-width: 8px;
}
.cls-3 {
fill: #fff;
}
.laptop {
fill: #424242;
stroke: #424242;
}
@media (prefers-color-scheme: dark) {
.laptop {
fill: #d6d6d6;
stroke: #d6d6d6;
}
}
</style>
</defs>
<g>
<path class="cls-1"
d="M1656,996.17c-62.9,0-124.32-15.87-177.6-45.9-48.31-27.23-88.43-65.09-116.63-109.96,42.24,16.15,87.02,24.34,133.29,24.34,191.24,0,346.83-142.61,346.83-317.91,0-49.29-17.77-79.71-40.27-118.22-.25-.44-.51-.87-.77-1.31,56.26,25.15,103.09,59.13,135.92,98.71,38.75,46.72,58.4,100.56,58.4,160,0,82.81-35.24,160.68-99.22,219.27-64.08,58.67-149.29,90.99-239.96,90.99Z" />
<path class="cls-2"
d="M1810.27,435.77c21.37,10.21,41.26,21.71,59.35,34.32,24.99,17.43,46.59,37.03,64.2,58.27,38.17,46.02,57.52,99.03,57.52,157.56,0,41.28-8.83,81.34-26.25,119.04-16.85,36.47-40.98,69.24-71.73,97.4-30.8,28.2-66.67,50.34-106.62,65.82-41.4,16.04-85.39,24.17-130.75,24.17-62.25,0-123.01-15.7-175.72-45.4-44.28-24.95-81.59-58.94-108.99-99.08,39.45,13.69,80.98,20.62,123.77,20.62,193.35,0,350.66-144.33,350.66-321.74,0-46.31-15.25-76.1-35.44-110.97M1791.69,419.07c25.27,43.77,46.37,74.72,46.37,127.67,0,173.46-153.57,314.08-343,314.08-50.83,0-99.07-10.14-142.46-28.3,57.52,99.6,171.79,167.48,303.4,167.48,189.44,0,343-140.62,343-314.08,0-126.92-88.98-217.31-207.31-266.85h0Z" />
</g>
<path class="cls-4"
d="M1850.7,210.11c40.63,49.7,33.28,122.93-16.42,163.56-49.7,40.63-174.15,75.16-214.78,25.46-40.63-49.7,17.94-164.81,67.64-205.44,49.7-40.63,122.93-33.28,163.56,16.42Z" />
<g>
<path class="cls-1"
d="M1141.23,996c-107.8,0-211.68-30.24-292.51-85.15-34.04-23.13-63.19-50-86.62-79.87-30.95-39.44-50.89-82.52-59.27-128.07,2.74-4.13,13.24-18.52,34.99-33.02,23.72-15.81,66.05-35,133.04-36.62,3.24-.07,6.3-.11,9.33-.11,43.77,0,86.56,14.88,130.79,45.49,38.84,26.88,73.68,62.33,104.42,93.61,24.59,25.02,47.83,48.66,69.78,64.67,62.27,45.4,122.66,67.87,162.35,78.74,26.53,7.27,47.34,10.45,59.7,11.84-35.66,20.71-74.9,37.05-116.83,48.62-47.77,13.19-97.96,19.88-149.17,19.88Z" />
<path class="cls-2"
d="M880.19,637.16c42.93,0,84.97,14.65,128.51,44.78,38.53,26.66,73.23,61.97,103.85,93.12,24.71,25.14,48.06,48.89,70.28,65.1,62.76,45.75,123.63,68.41,163.65,79.36,19.52,5.35,36,8.51,48.36,10.37-32.55,17.79-67.94,32.01-105.51,42.38-47.43,13.09-97.26,19.73-148.11,19.73-54.46,0-107.6-7.59-157.95-22.55-48.57-14.43-93.08-35.26-132.31-61.91-33.7-22.89-62.54-49.48-85.72-79.03-30.18-38.46-49.75-80.41-58.19-124.72,8.21-11.64,51.24-63.8,163.88-66.52,3.21-.07,6.24-.11,9.25-.11M880.19,629.16c-3.2,0-6.34.04-9.43.11-132.17,3.19-172.15,72.82-172.15,72.82,8.48,47.56,29.48,92.03,60.34,131.36,23.74,30.26,53.31,57.47,87.52,80.71,78.67,53.44,181.82,85.84,294.76,85.84,105.42,0,202.3-28.24,278.72-75.46,0,0-28.21-.92-71.36-12.74-43.16-11.81-101.26-34.52-161.05-78.11-76.68-55.9-167.65-204.53-307.35-204.53h0Z" />
</g>
<g>
<path class="cls-3"
d="M760.28,828.65c-29.91-38.81-49.23-81.08-57.46-125.74,2.74-4.13,13.24-18.52,34.99-33.02,23.38-15.59,64.87-34.46,130.24-36.54l51.71,133.53-159.48,61.77Z" />
<path class="cls-2"
d="M865.35,637.45l49.24,127.14-152.95,59.24c-28.13-37.18-46.48-77.52-54.58-120.04,8.07-11.44,49.8-62.07,158.29-66.35M870.76,629.27c-132.17,3.19-172.15,72.82-172.15,72.82,8.48,47.56,29.48,92.03,60.34,131.36l165.99-64.29-54.18-139.89h0Z" />
</g>
<rect class="cls-5 laptop" x="1219.11" y="766.83" width="270.32" height="9.95"
transform="translate(304.74 -380.67) rotate(18)" />
<rect class="cls-5 laptop" x="1059.2" y="596.18" width="270.32" height="9.95"
transform="translate(1479.19 -705.28) rotate(75.58)" />
<path class="cls-6"
d="M1666.04,416.09c1.06-29.87-22.29-54.95-52.17-56.01-2.32-.08-4.6,0-6.85.2.75,15,4.9,28.4,13.47,38.89,10.4,12.71,26.28,19.9,44.99,22.9.29-1.96.48-3.95.55-5.98Z" />
<path class="cls-4"
d="M1851.96,176.25c-29.01-25.87-78.84-33.24-78.84-33.24,0,0,37.28-38.45,83.99-62.26,46.65-23.78,102.73-32.92,102.73-32.92,0,0-26.34,46.29-43.76,93.12-19.06,51.23-22.35,98.62-22.35,98.62,0,0-13.99-38.55-41.77-63.33Z" />
<ellipse class="cls-2" cx="1700.37" cy="301.32" rx="10.19" ry="17.93"
transform="translate(271.38 1270.89) rotate(-44.21)" />
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

+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",
+3 -4
View File
@@ -1,14 +1,13 @@
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(forceSelected: boolean = false): 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
}); });
if (tabs.length < 2) if (!forceSelected && tabs.length < 2)
{ {
const ignorePinned: boolean = await settings.ignorePinned.getValue(); const ignorePinned: boolean = await settings.ignorePinned.getValue();
tabs = await browser.tabs.query({ tabs = await browser.tabs.query({
+8
View File
@@ -103,5 +103,13 @@ export const settings = {
fallback: true, fallback: true,
version: 1 version: 1
} }
),
compactView: storage.defineItem<boolean>(
"sync:compactView",
{
fallback: false,
version: 1
}
) )
}; };
+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
{ {
-1
View File
@@ -87,7 +87,6 @@ export default defineConfig({
id: "tabsaside@xfox111.net", id: "tabsaside@xfox111.net",
strict_min_version: "139.0", strict_min_version: "139.0",
// @ts-expect-error Introduced in Firefox 139
data_collection_permissions: { data_collection_permissions: {
required: ["browsingActivity"], required: ["browsingActivity"],
optional: ["technicalAndInteraction"] optional: ["technicalAndInteraction"]
-9523
View File
File diff suppressed because it is too large Load Diff