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

Compare commits

..

62 Commits

Author SHA1 Message Date
xfox111 e803636c35 chore: release candidate 7 2025-07-13 16:54:54 +03:00
xfox111 c8b4ef3e15 fix: tabs in collection list view overlap on overflow on Firefox 2025-07-13 16:50:02 +03:00
xfox111 eeefd1feff fix: collection context menus flickering in Firefox popup view 2025-07-13 16:20:17 +03:00
xfox111 53adbd4f75 fix: adding forbidden tabs via "Add selected tabs" 2025-07-11 13:53:27 +03:00
xfox111 3eed3b4b01 fix: list view horizontal overflow in sidebar on firefox 2025-07-09 00:49:09 +03:00
xfox111 525130b7e9 fix: collection collapses on tab/group drag 2025-07-06 12:34:02 +03:00
xfox111 1a274348e0 fix!: incorrect byte calculation for cloud storage (changed encoding) 2025-07-06 12:17:17 +03:00
xfox111 2c8cfa1583 chore: release candidate 6 2025-07-03 12:58:37 +03:00
xfox111 e844a68f49 chore(ui): remove fixed collection height for list view 2025-07-03 12:56:58 +03:00
xfox111 794f6e3af0 chore(locale): clarify action icon option for slaivc locales (ru, uk, pl) 2025-07-03 12:06:25 +03:00
xfox111 d85d10dc58 chore(ui): increase tab's dnd handle area 2025-07-03 11:46:53 +03:00
xfox111 213cc84602 chore: reduce delay and increase tolerance for mouse drag and drop 2025-07-03 11:33:05 +03:00
xfox111 9675b65e81 fix: pinned tab opens and closes infinitely if pwa window is opened 2025-07-03 11:24:53 +03:00
xfox111 6bce330a8f chore: dependabot reviewers deprecation 2025-06-03 18:55:01 +03:00
xfox111 405f9163f2 !feat: tabGroups API for Firefox 139 2025-05-23 16:45:50 +03:00
xfox111 e498e25c57 chore: version update 2025-05-08 10:07:18 +03:00
xfox111 d07c99e3a1 !feat: partial tab groups support on firefox 2025-05-08 10:06:47 +03:00
xfox111 0ff1d63cde fix: close firefox sidebar when list location changes 2025-05-08 10:05:53 +03:00
xfox111 dfcafae2b1 fix: options page crush on cloud storage disabling 2025-05-08 09:05:53 +03:00
xfox111 8f4cd4198a fix: groupig fails on private windows when group contains restricted tabs 2025-05-08 09:05:21 +03:00
xfox111 1b64f65e9f fix: empty collection/group title 2025-05-08 09:04:34 +03:00
xfox111 d249e07eca fix: corrupted json export 2025-05-08 09:04:08 +03:00
xfox111 4070907240 chore(loc): english locale fix 2025-05-08 09:03:15 +03:00
xfox111 9d250dc01d fix: MAX_CAPTURE_VISIBLE_TAB_CALLS_PER_SECOND exceeded 2025-05-08 06:59:55 +03:00
xfox111 24bf0e88ca Fix: window closes if all tabs were set aside 2025-05-08 06:56:06 +03:00
xfox111 ef94842066 fix: collections removed by their sorted index 2025-05-08 06:55:35 +03:00
xfox111 40490aec2d fix: open collection in a new window doesn't work 2025-05-08 02:26:08 +03:00
xfox111 06aca3d3ca fix: show timestamp-based title in edit dialog title placeholder 2025-05-08 02:20:44 +03:00
xfox111 a6a5c236c6 fix: broken chrome store link 2025-05-08 02:09:22 +03:00
xfox111 a144221e33 chore: chunk max size warning 2025-05-07 20:38:18 +03:00
xfox111 e6a69980c2 docs: privacy policy 2025-05-07 20:26:24 +03:00
xfox111 59b0547ec6 chore: version update 2025-05-07 20:01:17 +03:00
xfox111 eed5159a56 chore(loc): simplified chinese translation 2025-05-07 19:55:28 +03:00
xfox111 7effe309dd chore: minor updates 2025-05-07 19:40:03 +03:00
xfox111 6728a50056 chore(loc): spanish translation 2025-05-07 19:39:12 +03:00
xfox111 0cb036c69a chore(loc): italian translation 2025-05-07 19:18:48 +03:00
xfox111 4ef336da5b chore(loc): polish translation 2025-05-07 18:43:50 +03:00
xfox111 00492ad710 chore: tab list location logic improvement #117 2025-05-07 00:50:54 +03:00
xfox111 a706c3bc89 fix: when listLocation is "tab" or "pinned" add all tabs instead of selected #117 2025-05-07 00:28:48 +03:00
xfox111 4ef9e2651c fix: partial save notification appears when listLocation is "tab" or "pinned" 2025-05-07 00:27:16 +03:00
xfox111 bfb849fbdf fix: collection list refresh across multiple list views 2025-05-07 00:09:50 +03:00
xfox111 297a6aa95c feat: allow to create any group on firefox 2025-05-07 00:05:12 +03:00
xfox111 aa2ee02c79 chore: messenger refactoring 2025-05-07 00:04:19 +03:00
xfox111 8b77159abe hotfix: wrapped analytics into try/catch to prevent failing on firefox 2025-05-07 00:03:43 +03:00
xfox111 f5bf0db039 !feat: remove "pointer: coarse" media #117 2025-05-06 22:58:52 +03:00
xfox111 5d4a59153a feat: ga4 analytics #117 2025-05-05 19:25:25 +03:00
Maison da Silva b6be86aac9 locales pt_BR.yml
locales pt_BR.yml
2025-05-04 19:51:19 +03:00
Maison da Silva d872515b8b Update pt_BR.txt 2025-05-04 19:50:59 +03:00
Maison da Silva 8693e8d563 Create pt_BR.txt 2025-05-04 19:50:59 +03:00
xfox111 9c4121ea79 chore(ci): exclude locales from pr check 2025-05-04 19:50:29 +03:00
xfox111 f89d036ab8 feat: captureVisibleTab fallback for tab previews 2025-05-04 15:22:33 +03:00
xfox111 e59782973b feat: ability to disable cloud collection storage 2025-05-04 14:21:10 +03:00
xfox111 70ed16c286 chore: remove web-ext.config.js 2025-05-04 14:18:22 +03:00
xfox111 db78314a44 feat: remove context menu from "page" context #117 2025-05-04 13:17:01 +03:00
xfox111 a478352ca3 chore: cd pipeline refactoring 2025-05-04 11:15:04 +03:00
xfox111 2eba532901 chore: firefox extension id fix 2025-05-04 11:12:11 +03:00
xfox111 1e60b776c4 chore: npm scripts cleanup 2025-05-04 10:29:30 +03:00
xfox111 0c18a3de5a fix: firefox badge background color 2025-05-04 10:26:50 +03:00
xfox111 4e40742755 fix: partial save notification doesn't appear on firefox 2025-05-04 10:26:20 +03:00
xfox111 16023ac152 fix: clicking action button on firefox doesn't open sidebar 2025-05-04 10:25:31 +03:00
xfox111 39793a38c3 !feat: major 3.0 release candidate 2025-05-03 23:59:43 +03:00
xfox111 dbc8c7fd4d init: nuke the repo 2025-05-03 09:19:36 +03:00
106 changed files with 7685 additions and 13016 deletions
+1 -1
View File
@@ -22,5 +22,5 @@
} }
}, },
"postCreateCommand": "npm install" "postCreateCommand": "yarn install"
} }
+3 -35
View File
@@ -16,33 +16,7 @@ updates:
schedule: schedule:
interval: monthly interval: monthly
rebase-strategy: disabled rebase-strategy: disabled
groups: open-pull-requests-limit: 20
deps:
patterns:
- "*"
exclude-patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
- "scheduler"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
update-types:
- minor
- patch
react-next:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
update-types:
- major
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
@@ -52,10 +26,7 @@ updates:
schedule: schedule:
interval: monthly interval: monthly
rebase-strategy: disabled rebase-strategy: disabled
groups: open-pull-requests-limit: 20
actions:
patterns:
- "*"
- package-ecosystem: "devcontainers" - package-ecosystem: "devcontainers"
directory: "/" directory: "/"
@@ -65,7 +36,4 @@ updates:
schedule: schedule:
interval: monthly interval: monthly
rebase-strategy: disabled rebase-strategy: disabled
groups: open-pull-requests-limit: 20
devcontainers:
patterns:
- "*"
+7 -19
View File
@@ -38,7 +38,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:24 container: node:20
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -51,19 +51,8 @@ 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: npm install - run: yarn install
- run: yarn zip -b ${{ matrix.target }}
# 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
working-directory: ./node_modules/@dnd-kit/core/dist
if: ${{ matrix.target == 'firefox' }}
# Patch for firefox analytics (see https://github.com/wxt-dev/wxt/pull/1808)
- run: sed -i 's|if (location.pathname === "/background.js")|if (location.pathname === "/background.js" \|\| location.pathname === "/_generated_background_page.html")|' index.mjs
working-directory: ./node_modules/@wxt-dev/analytics/dist
if: ${{ matrix.target == 'firefox' }}
- run: 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
@@ -74,13 +63,12 @@ jobs:
- name: web-ext lint - name: web-ext lint
if: ${{ matrix.target == 'firefox' }} if: ${{ matrix.target == 'firefox' }}
uses: kewisch/action-web-ext@main uses: freaktechnik/web-ext-lint@main
with: with:
cmd: lint extension-root: ./.output/firefox-mv3
source: ./.output/firefox-mv3 self-hosted: false
channel: listed
- run: npm audit - run: yarn 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:
+4 -4
View File
@@ -52,11 +52,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@main uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v4 uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell. # ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,4 +83,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v3
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
extver=`jq -r ".version" package.json` extver=`jq -r ".version" package.json`
echo "version=$extver" >> "$GITHUB_OUTPUT" echo "version=$extver" >> "$GITHUB_OUTPUT"
- uses: dev-build-deploy/release-me@v0.18.2 - uses: dev-build-deploy/release-me@v0.18.0
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
prefix: v prefix: v
+7 -19
View File
@@ -30,7 +30,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: node:24 container: node:23
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -43,19 +43,8 @@ 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: npm install - run: yarn install
- run: yarn zip -b ${{ matrix.target }}
# 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
working-directory: ./node_modules/@dnd-kit/core/dist
if: ${{ matrix.target == 'firefox' }}
# Patch for firefox analytics (see https://github.com/wxt-dev/wxt/pull/1808)
- run: sed -i 's|if (location.pathname === "/background.js")|if (location.pathname === "/background.js" \|\| location.pathname === "/_generated_background_page.html")|' index.mjs
working-directory: ./node_modules/@wxt-dev/analytics/dist
if: ${{ matrix.target == 'firefox' }}
- run: 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
@@ -66,10 +55,9 @@ jobs:
- name: web-ext lint - name: web-ext lint
if: ${{ matrix.target == 'firefox' }} if: ${{ matrix.target == 'firefox' }}
uses: kewisch/action-web-ext@main uses: freaktechnik/web-ext-lint@main
with: with:
cmd: lint extension-root: ./.output/firefox-mv3
source: ./.output/firefox-mv3 self-hosted: false
channel: listed
- run: npm audit - run: yarn audit
-1
View File
@@ -8,7 +8,6 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
.yarn/
.output .output
stats.html stats.html
stats-*.json stats-*.json
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2026 Eugene Fox Copyright (c) 2025 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
+2 -35
View File
@@ -6,7 +6,7 @@
- Thumbnails of saved tabs - Thumbnails of saved tabs
3. This extension uses Google Analytics to collect usage statistics and improve the extension. 3. This extension uses Google Analytics to collect usage statistics and improve the extension.
4. This extension uses analytics to collect following data: 4. This extension uses analytics to collect following data:
- Random UUID to distinguish unique users - Random UUID to identify the user
- Browser name and version - Browser name and version
- Operating system name and version - Operating system name and version
- System architecture - System architecture
@@ -14,40 +14,7 @@
- Extension language - Extension language
- User settings - User settings
- Number of saved collections - Number of saved collections
- Events, related to user's actions: - Action identifiers (e.g. "page_view", "extension_installed", "item_created", etc.)
- `bmc_clicked` (when "Buy me a Coffee" button is clicked)
- `collection_list` (when extension's options page is opened)
- `cta_dismissed` (when "Like this extension?" prompt is closed)
- `extension_installed` (when extension is installed or updated)
- `feedback_clicked` (when "Leave feedback" button is clicked)
- `item_created` (when new collection or group is created using dialog window)
- `item_edited` (when collection or group is edited)
- `options_page` (when extension's options page is opened)
- `page_view` (when extension's page is opened)
- `save` (when "Save all tabs" or "Save selected tabs" buttons are clicked)
- `set_aside` (when "Set all tabs aside" or "Set selected tabs aside" buttons are clicked)
- `used_drag_and_drop` (when items inside collection list were reordered)
- `visit_blog_button_click` (when "Read dev blog" button is clicked)
- `bookmarks_saved` (when "Export to bookmarks" option is clicked)
- Events, related to extension errors:
- `background_error` (when error inside background service has occured)
- `cloud_get_error` (when failed to retrieve collections from the cloud storage)
- `conflict_resolve_with_cloud_error` (when failed to retrieve collections from the cloud storage during storage conflict resolution)
- `cloud_save_error` (when failed to save collections to the cloud storage)
- `messaging_error` (when failed to send a message to extenion's background service)
- `notification_error` (when failed to display a toast notification)
4. Following events, beside their name, include additional information, such as:
- `item_created` and `item_edited`:
- Type of the affected item (`collection` or `group`)
- `extension_installed`:
- Reason for update (`install`, `update`, or `browser_update`)
- Previously installed extension's version, if applicable
- `page_view`:
- Type of the page (`options_page` or `collection_list`)
- All extension's error events:
- Error name
- Error message
- Error call stack
4. This extension does not collect or use any personally identifiable information (PII) or sensitive data for purposes other than its core functionality. 4. This extension does not collect or use any personally identifiable information (PII) or sensitive data for purposes other than its core functionality.
5. This extension uses cloud storage built into your browser to store its data. 5. This extension uses cloud storage built into your browser to store its data.
6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser. 6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
+3 -3
View File
@@ -37,7 +37,7 @@ Check out our [latest blog post](https://at.xfox111.net/tabs-aside-3-0) regardin
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin) - [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin)
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd) - [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd)
- [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/ms-edge-tabs-aside/) - [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/)
- [GitHub Releases](https://github.com/xfox111/TabsAsideExtension/releases/latest) - [GitHub Releases](https://github.com/xfox111/TabsAsideExtension/releases/latest)
### Sideloading (for testing purposes only) ### Sideloading (for testing purposes only)
@@ -81,7 +81,7 @@ If you want to sideload it without replacing to run both versions at the same ti
## Contributing ## Contributing
[![GitHub issues](https://img.shields.io/github/issues/xfox111/TabsAsideExtension)](https://github.com/xfox111/TabsAsideExtension/issues) [![GitHub issues](https://img.shields.io/github/issues/xfox111/TabsAsideExtension)](https://github.com/xfox111/TabsAsideExtension/issues)
[![CI](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yml/badge.svg)](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yml) [![CI](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yaml/badge.svg)](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yaml)
[![GitHub repo size](https://img.shields.io/github/repo-size/xfox111/TabsAsideExtension?label=repo%20size)](https://github.com/xfox111/TabsAsideExtension) [![GitHub repo size](https://img.shields.io/github/repo-size/xfox111/TabsAsideExtension?label=repo%20size)](https://github.com/xfox111/TabsAsideExtension)
There are many ways in which you can participate in the project, for example: There are many ways in which you can participate in the project, for example:
@@ -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)
> ©2026 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/TabsAsideExtension/blob/main/LICENSE) > ©2025 Eugene Fox. Licensed under [MIT license](https://github.com/XFox111/TabsAsideExtension/blob/main/LICENSE)
+24
View File
@@ -0,0 +1,24 @@
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
import { WxtAppConfig } from "wxt/sandbox";
import { userPropertiesStorage } from "./features/analytics";
export default defineAppConfig({
analytics:
{
debug: import.meta.env.DEV,
enabled: storage.defineItem("local:analytics", {
fallback: true
}),
userId: storage.defineItem("local:userId", {
init: () => crypto.randomUUID()
}),
userProperties: userPropertiesStorage,
providers:
[
googleAnalytics4({
apiSecret: import.meta.env.WXT_GA4_API_SECRET,
measurementId: import.meta.env.WXT_GA4_MEASUREMENT_ID
})
]
}
} as WxtAppConfig);
-3
View File
@@ -39,19 +39,16 @@ body
/* Handle */ /* Handle */
::-webkit-scrollbar-thumb ::-webkit-scrollbar-thumb
{ {
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1); background-color: var(--colorNeutralStroke1);
border-radius: 4px; border-radius: 4px;
} }
::-webkit-scrollbar-thumb:hover ::-webkit-scrollbar-thumb:hover
{ {
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1Hover); background-color: var(--colorNeutralStroke1Hover);
} }
::-webkit-scrollbar-thumb:hover:active ::-webkit-scrollbar-thumb:hover:active
{ {
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1Pressed); background-color: var(--colorNeutralStroke1Pressed);
} }
+2 -3
View File
@@ -13,13 +13,12 @@ export const githubLinks =
repo: githubLink(), repo: githubLink(),
release: githubLink(`releases/tag/v${Package.version}`), release: githubLink(`releases/tag/v${Package.version}`),
license: githubLink("blob/main/LICENSE"), license: githubLink("blob/main/LICENSE"),
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations"), translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations")
privacy: githubLink("blob/main/PRIVACY.md")
}; };
export const storeLink: string = 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/" :
browser.runtime.getManifest().update_url?.startsWith("https://edge.microsoft.com/") ? chrome.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";
+46 -150
View File
@@ -1,32 +1,24 @@
import { track, trackError } from "@/features/analytics"; import { track, trackError } from "@/features/analytics";
import { collectionCount, getCollections, saveCollections, thumbnailCaptureEnabled } from "@/features/collectionStorage"; import { collectionCount, getCollections, saveCollections } 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 { 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 saveTabsToCollection from "@/utils/saveTabsToCollection";
import sendNotification from "@/utils/sendNotification"; import sendNotification from "@/utils/sendNotification";
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 { Tabs, Windows } from "wxt/browser";
import { Unwatch } from "wxt/utils/storage"; import { Unwatch } from "wxt/storage";
import { openCollection, openGroup } from "./sidepanel/utils/opener";
export default defineBackground(() => export default defineBackground(() =>
{ {
try try
{ {
const logger = getLogger("background"); const logger = getLogger("background");
let graphicsCache: GraphicsStorage = {}; const graphicsCache: GraphicsStorage = {};
let listLocation: SettingsValue<"listLocation"> = "sidebar"; let listLocation: SettingsValue<"listLocation"> = "sidebar";
logger("Background script started"); logger("Background script started");
@@ -41,42 +33,26 @@ export default defineBackground(() =>
logger("onInstalled", reason, previousVersion); logger("onInstalled", reason, previousVersion);
track("extension_installed", { reason, previousVersion: previousVersion ?? "none" }); track("extension_installed", { reason, previousVersion: previousVersion ?? "none" });
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt); const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
const cumulative: number = major * 10000 + minor * 100 + patch;
await setSettingsReviewNeeded(reason, previousVersion); if (reason === "update" && previousMajor < 3)
if (reason === "update" && cumulative < 30000) // < 3.0.0
{ {
await migrateStorage(); await migrateStorage();
await showWelcomeDialog.setValue(true); await showWelcomeDialog.setValue(true);
browser.runtime.reload(); browser.runtime.reload();
} }
});
if (reason === "update" && cumulative >= 30000 && cumulative < 30200) // >= 3.0.0 && < 3.2.0 browser.tabs.onUpdated.addListener((_, __, tab) =>
{ {
// Merge cloud and local storage if they are out of sync if (!tab.url)
const localTimestamp: number = await collectionStorage.localLastUpdated.getValue(); return;
const syncTimestamp: number = await collectionStorage.syncLastUpdated.getValue();
if (localTimestamp === syncTimestamp) graphicsCache[tab.url] = {
return; preview: graphicsCache[tab.url]?.preview,
capture: graphicsCache[tab.url]?.capture,
try icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
{ };
const localCollections: CollectionItem[] = await getCollectionsFromLocal();
const cloudCollections: CollectionItem[] = await getCollectionsFromCloud();
const mergedCollections: CollectionItem[] = [...cloudCollections, ...localCollections];
await saveCollections(mergedCollections, true, graphicsCache);
}
catch (ex)
{
logger("Failed to merge cloud and local storage during update");
trackError("cloud_sync_merge_error", ex as Error);
console.error(ex);
}
}
}); });
browser.commands.onCommand.addListener( browser.commands.onCommand.addListener(
@@ -84,33 +60,20 @@ export default defineBackground(() =>
); );
onMessage("getGraphicsCache", () => graphicsCache); onMessage("getGraphicsCache", () => graphicsCache);
onMessage("refreshCollections", () => { }); onMessage("addThumbnail", ({ data }) =>
if (import.meta.env.FIREFOX)
{ {
onMessage("openCollection", ({ data }) => openCollection(data.collection, data.targetWindow)); graphicsCache[data.url] = {
onMessage("openGroup", ({ data }) => openGroup(data.group, data.newWindow)); preview: data.thumbnail,
} capture: graphicsCache[data.url]?.capture,
icon: graphicsCache[data.url]?.icon
};
});
onMessage("refreshCollections", () => {});
setupTabCaputre(); setupTabCaputre();
async function setupTabCaputre(): Promise<void> async function setupTabCaputre(): Promise<void>
{ {
let unwatchAddThumbnail: RemoveListenerCallback | null = null; const tryCaptureTab = async (tab: Tabs.Tab): Promise<void> =>
let captureInterval: NodeJS.Timeout | null = null;
const captureFavicon = (_: any, __: any, tab: Browser.tabs.Tab): void =>
{
if (!tab.url)
return;
graphicsCache[tab.url] = {
preview: graphicsCache[tab.url]?.preview,
capture: graphicsCache[tab.url]?.capture,
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
};
};
const tryCaptureTab = async (tab: Browser.tabs.Tab): Promise<void> =>
{ {
if (!tab.url || tab.status !== "complete" || !tab.active) if (!tab.url || tab.status !== "complete" || !tab.active)
return; return;
@@ -122,7 +85,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 browser.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 }); const capture: string = await chrome.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
if (capture) if (capture)
{ {
@@ -143,61 +106,11 @@ export default defineBackground(() =>
} }
}; };
const updateCapture = async (captureThumbnails: boolean): Promise<void> => setInterval(() =>
{ {
const scriptingGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"] }); browser.tabs.query({ active: true })
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
if (captureThumbnails) }, 1000);
{
if (scriptingGranted)
await browser.scripting.registerContentScripts([
{
id: "capture-script",
matches: ["<all_urls>"],
runAt: "document_idle",
js: ["capture.js"]
}
]);
unwatchAddThumbnail = onMessage("addThumbnail", ({ data }) =>
{
graphicsCache[data.url] = {
preview: data.thumbnail,
capture: graphicsCache[data.url]?.capture,
icon: graphicsCache[data.url]?.icon
};
});
captureInterval = setInterval(() =>
{
browser.tabs.query({ active: true })
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
}, 1000);
browser.tabs.onUpdated.addListener(captureFavicon);
}
else
{
if (scriptingGranted)
await browser.scripting.unregisterContentScripts({
ids: ["capture-script"]
});
unwatchAddThumbnail?.();
if (captureInterval)
clearInterval(captureInterval);
browser.tabs.onUpdated.removeListener(captureFavicon);
graphicsCache = {};
}
};
if (await thumbnailCaptureEnabled.getValue())
updateCapture(true);
thumbnailCaptureEnabled.watch(updateCapture);
} }
setupContextMenu(); setupContextMenu();
@@ -286,7 +199,6 @@ 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 +215,7 @@ export default defineBackground(() =>
unwatchActionTitle?.(); unwatchActionTitle?.();
if (!import.meta.env.FIREFOX) if (!import.meta.env.FIREFOX)
await browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
// Setup new behavior // Setup new behavior
if (action === "action") if (action === "action")
@@ -322,7 +234,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
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
} }
else if (location !== "popup") else if (location !== "popup")
browser.action.onClicked.addListener(openCollectionsInTab); browser.action.onClicked.addListener(openCollectionsInTab);
@@ -341,17 +253,17 @@ export default defineBackground(() =>
{ {
logger("enforcePinnedTab"); logger("enforcePinnedTab");
const openWindows: Browser.windows.Window[] = await browser.windows.getAll({ populate: true }); const openWindows: 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: Browser.tabs.Tab[] = openWindow.tabs!.filter(tab => const activeTabs: Tabs.Tab[] = openWindow.tabs!.filter(tab =>
tab.url === browser.runtime.getURL("/sidepanel.html")); tab.url === browser.runtime.getURL("/sidepanel.html"));
const targetTab: Browser.tabs.Tab | undefined = activeTabs.find(tab => tab.pinned); const targetTab: Tabs.Tab | undefined = activeTabs.find(tab => tab.pinned);
if (!targetTab) if (!targetTab)
await browser.tabs.create({ await browser.tabs.create({
@@ -361,7 +273,7 @@ export default defineBackground(() =>
pinned: true pinned: true
}); });
const tabsToClose: Browser.tabs.Tab[] = activeTabs.filter(tab => tab.id !== targetTab?.id); const tabsToClose: 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 +285,7 @@ export default defineBackground(() =>
logger("updateView", viewLocation); logger("updateView", viewLocation);
browser.tabs.onHighlighted.removeListener(enforcePinnedTab); browser.tabs.onHighlighted.removeListener(enforcePinnedTab);
const tabs: Browser.tabs.Tab[] = await browser.tabs.query({ const tabs: 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,12 +295,11 @@ 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 browser.sidePanel.setOptions({ enabled: viewLocation === "sidebar" }); await chrome.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
if (viewLocation === "pinned") if (viewLocation === "pinned")
{ {
@@ -419,10 +330,9 @@ 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
browser.sidePanel.open({ windowId }); chrome.sidePanel.open({ windowId });
} }
else else
browser.action.openPopup(); browser.action.openPopup();
@@ -432,11 +342,11 @@ export default defineBackground(() =>
{ {
logger("openCollectionsInTab"); logger("openCollectionsInTab");
const currentWindow: Browser.windows.Window = await browser.windows.getCurrent({ populate: true }); const currentWindow: Windows.Window = await browser.windows.getCurrent({ populate: true });
if (currentWindow.incognito) if (currentWindow.incognito)
{ {
let availableWindows: Browser.windows.Window[] = await browser.windows.getAll({ populate: true }); let availableWindows: Windows.Window[] = await browser.windows.getAll({ populate: true });
availableWindows = availableWindows.filter(window => availableWindows = availableWindows.filter(window =>
!window.incognito && !window.incognito &&
@@ -445,7 +355,7 @@ export default defineBackground(() =>
if (availableWindows.length > 0) if (availableWindows.length > 0)
{ {
const availableTab: Browser.tabs.Tab = availableWindows[0].tabs!.find( const availableTab: Tabs.Tab = availableWindows[0].tabs!.find(
tab => tab.url === browser.runtime.getURL("/sidepanel.html") tab => tab.url === browser.runtime.getURL("/sidepanel.html")
)!; )!;
@@ -462,7 +372,7 @@ export default defineBackground(() =>
} }
else else
{ {
const collectionTab: Browser.tabs.Tab | undefined = currentWindow.tabs!.find( const collectionTab: Tabs.Tab | undefined = currentWindow.tabs!.find(
tab => tab.url === browser.runtime.getURL("/sidepanel.html") tab => tab.url === browser.runtime.getURL("/sidepanel.html")
); );
@@ -481,28 +391,14 @@ export default defineBackground(() =>
{ {
logger("saveTabs", closeAfterSave); logger("saveTabs", closeAfterSave);
const [tabs, skipCount] = await getTabsToSaveAsync(); const collection: CollectionItem = await saveTabsToCollection(closeAfterSave);
if (tabs.length < 1)
{
await sendPartialSaveNotification();
return;
}
const collection: CollectionItem = await createCollectionFromTabs(tabs);
const [savedCollections, cloudIssue] = await getCollections(); const [savedCollections, cloudIssue] = await getCollections();
const newList = [collection, ...savedCollections]; const newList = [collection, ...savedCollections];
await saveCollections(newList, cloudIssue === null, graphicsCache); await saveCollections(newList, cloudIssue === null, graphicsCache);
track(closeAfterSave ? "set_aside" : "save");
sendMessage("refreshCollections", undefined); sendMessage("refreshCollections", undefined);
if (skipCount > 0)
await sendPartialSaveNotification();
if (closeAfterSave)
await closeTabsAsync(tabs);
if (await settings.notifyOnSave.getValue()) if (await settings.notifyOnSave.getValue())
await sendNotification({ await sendNotification({
title: i18n.t("notifications.tabs_saved.title"), title: i18n.t("notifications.tabs_saved.title"),
@@ -4,7 +4,11 @@ import { sendMessage } from "@/utils/messaging";
// This content script is injected into each browser tab. // This content script is injected into each browser tab.
// It's purpose is to retrive an OpenGraph thumbnail URL from the metadata // It's purpose is to retrive an OpenGraph thumbnail URL from the metadata
export default defineUnlistedScript({ main }); export default defineContentScript({
matches: ["<all_urls>"],
runAt: "document_idle",
main
});
const logger = getLogger("contentScript"); const logger = getLogger("contentScript");
@@ -34,18 +34,5 @@ export const useOptionsStyles = makeStyles({
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
gap: tokens.spacingHorizontalS gap: tokens.spacingHorizontalS
},
group:
{
display: "flex",
flexFlow: "column",
alignItems: "flex-start",
gap: tokens.spacingVerticalSNudge
},
img:
{
height: "100px",
flexGrow: 1,
alignSelf: "flex-end"
} }
}); });
+19 -22
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, Image, Link, Subtitle1, Text } from "@fluentui/react-components"; import { Body1, Button, Caption1, 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,6 +19,24 @@ 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>
</Body1>
<div className={ cls.horizontalButtons }> <div className={ cls.horizontalButtons }>
<Button <Button
as="a" { ...extLink(storeLink) } as="a" { ...extLink(storeLink) }
@@ -35,27 +53,6 @@ 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,4 +1,3 @@
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";
@@ -14,25 +13,9 @@ export default function GeneralSection(): React.ReactElement
const [dismissOnLoad, setDismissOnLoad] = useSettings("dismissOnLoad"); const [dismissOnLoad, setDismissOnLoad] = useSettings("dismissOnLoad");
const [listLocation, setListLocation] = useSettings("listLocation"); const [listLocation, setListLocation] = useSettings("listLocation");
const [contextAction, setContextAction] = useSettings("contextAction"); const [contextAction, setContextAction] = useSettings("contextAction");
const [showPartialSaveNotification, setShowPartialSaveNotification] = useSettings("showPartialSaveNotification");
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
const cls = useOptionsStyles(); const cls = useOptionsStyles();
useEffect(() =>
{
analyticsPermission.getValue().then(setAllowAnalytics);
return analyticsPermission.watch(setAllowAnalytics);
}, []);
const updateAnalytics = (enabled: boolean): void =>
{
setAllowAnalytics(null);
analyticsPermission.setValue(enabled)
.catch(() => setAllowAnalytics(!enabled));
};
const openShortcutsPage = (): Promise<any> => const openShortcutsPage = (): Promise<any> =>
browser.tabs.create({ browser.tabs.create({
url: "chrome://extensions/shortcuts", url: "chrome://extensions/shortcuts",
@@ -45,7 +28,6 @@ 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);
@@ -74,20 +56,10 @@ export default function GeneralSection(): React.ReactElement
label={ i18n.t("options_page.general.options.show_notification") } label={ i18n.t("options_page.general.options.show_notification") }
checked={ notifyOnSave ?? false } checked={ notifyOnSave ?? false }
onChange={ (_, e) => setNotifyOnSave(e.checked as boolean) } /> onChange={ (_, e) => setNotifyOnSave(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.show_partial_save_notification") }
checked={ showPartialSaveNotification ?? false }
onChange={ (_, e) => setShowPartialSaveNotification(e.checked as boolean) } />
<Checkbox <Checkbox
label={ i18n.t("options_page.general.options.unload_tabs") } label={ i18n.t("options_page.general.options.unload_tabs") }
checked={ dismissOnLoad ?? false } checked={ dismissOnLoad ?? false }
onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } /> onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.allow_analytics") }
checked={ allowAnalytics ?? true }
disabled={ allowAnalytics === null }
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
</section> </section>
<Field label={ i18n.t("options_page.general.options.list_locations.title") }> <Field label={ i18n.t("options_page.general.options.list_locations.title") }>
+3 -53
View File
@@ -1,10 +1,9 @@
import { useDialog } from "@/contexts/DialogProvider"; import { useDialog } from "@/contexts/DialogProvider";
import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage"; import { cloudDisabled, setCloudStorage } from "@/features/collectionStorage";
import { useDangerStyles } from "@/hooks/useDangerStyles"; import { useDangerStyles } from "@/hooks/useDangerStyles";
import useStorageInfo from "@/hooks/useStorageInfo"; import useStorageInfo from "@/hooks/useStorageInfo";
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components"; import { Button, Field, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar } 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";
@@ -14,7 +13,6 @@ export default function StorageSection(): React.ReactElement
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo(); const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
const [importResult, setImportResult] = useState<boolean | null>(null); const [importResult, setImportResult] = useState<boolean | null>(null);
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!); const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
const [isThumbnailCaptureEnabled, setThumbnailCaptureEnabled] = useState<boolean | null>(null);
const dialog = useDialog(); const dialog = useDialog();
const cls = useOptionsStyles(); const cls = useOptionsStyles();
@@ -22,35 +20,10 @@ export default function StorageSection(): React.ReactElement
useEffect(() => useEffect(() =>
{ {
thumbnailCaptureEnabled.getValue().then(setThumbnailCaptureEnabled);
cloudDisabled.getValue().then(setCloudDisabled); cloudDisabled.getValue().then(setCloudDisabled);
return cloudDisabled.watch(setCloudDisabled);
const unwatchCloud: Unwatch = cloudDisabled.watch(setCloudDisabled);
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setThumbnailCaptureEnabled);
return () =>
{
unwatchCloud();
unwatchThumbnails();
};
}, []); }, []);
const handleSetThumbnailCapture = (enabled: boolean): void =>
{
setThumbnailCaptureEnabled(null);
thumbnailCaptureEnabled.setValue(enabled)
.catch(() => setThumbnailCaptureEnabled(!enabled));
};
const handleClearThumbnails = (): void =>
dialog.pushPrompt({
title: i18n.t("options_page.storage.clear_thumbnails.title"),
content: i18n.t("options_page.storage.clear_thumbnails.prompt"),
confirmText: i18n.t("common.actions.delete"),
destructive: true,
onConfirm: () => clearGraphicsStorage()
});
const handleImport = (): void => const handleImport = (): void =>
dialog.pushPrompt({ dialog.pushPrompt({
title: i18n.t("options_page.storage.import_prompt.title"), title: i18n.t("options_page.storage.import_prompt.title"),
@@ -78,29 +51,6 @@ export default function StorageSection(): React.ReactElement
return ( return (
<> <>
<div className={ cls.group }>
<Switch
checked={ isThumbnailCaptureEnabled ?? true }
disabled={ isThumbnailCaptureEnabled === null }
onChange={ (_, e) => handleSetThumbnailCapture(e.checked as boolean) }
label={ {
children: (_: any, props: LabelProps) =>
<InfoLabel
{ ...props }
label={ i18n.t("options_page.storage.thumbnail_capture") }
info={
<p>
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }<br /><br />
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
</p>
} />
} } />
<Button onClick={ handleClearThumbnails } className={ dangerCls.buttonSubtle } appearance="subtle">
{ i18n.t("options_page.storage.clear_thumbnails.action") }
</Button>
</div>
{ isCloudDisabled === false && { isCloudDisabled === false &&
<Field <Field
label={ i18n.t("options_page.storage.capacity.title") } label={ i18n.t("options_page.storage.capacity.title") }
+1 -2
View File
@@ -1,6 +1,5 @@
import App from "@/App.tsx"; import App from "@/App.tsx";
import "@/assets/global.css"; import "@/assets/global.css";
import { trackPage } from "@/features/analytics";
import { Tab, TabList } from "@fluentui/react-components"; import { Tab, TabList } from "@fluentui/react-components";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts"; import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
@@ -15,7 +14,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</App> </App>
); );
trackPage("options_page"); analytics.page("options_page");
function OptionsPage(): React.ReactElement function OptionsPage(): React.ReactElement
{ {
-5
View File
@@ -37,12 +37,7 @@ export default async function importData(): Promise<boolean | null>
await browser.storage.local.set(data.local); await browser.storage.local.set(data.local);
if (data.sync) if (data.sync)
{
if (import.meta.env.FIREFOX && data.sync.contextAction === "context")
data.sync.contextAction = "open";
await browser.storage.sync.set(data.sync); await browser.storage.sync.set(data.sync);
}
} }
catch (error) catch (error)
{ {
@@ -19,11 +19,6 @@ export const useStyles_CollectionView = makeStyles({
"&:hover": "&:hover":
{ {
boxShadow: tokens.shadow4 boxShadow: tokens.shadow4
},
"&:not(:focus-within) .compact":
{
display: "none"
} }
}, },
color: color:
@@ -12,12 +12,7 @@ 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({ export default function CollectionView({ collection, index: collectionIndex, dragOverlay }: CollectionViewProps): ReactElement
collection,
index: collectionIndex,
dragOverlay,
compact
}: CollectionViewProps): ReactElement
{ {
const { tilesView } = useCollections(); const { tilesView } = useCollections();
const { const {
@@ -58,12 +53,12 @@ export default function CollectionView({
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay && { (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
<> <>
{ collection.items.length < 1 ? { collection.items.length < 1 ?
<div className={ mergeClasses(cls.empty, compact === true && "compact") }> <div className={ cls.empty }>
<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, compact === true && "compact") }> <div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
<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 }
@@ -71,12 +66,9 @@ export default function CollectionView({
{ 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 <TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
key={ index } tab={ i } indices={ [collectionIndex, index] }
collectionId={ collection.timestamp } />
) } ) }
</SortableContext> </SortableContext>
</div> </div>
@@ -93,5 +85,4 @@ export type CollectionViewProps =
collection: CollectionItem; collection: CollectionItem;
index: number; index: number;
dragOverlay?: boolean; dragOverlay?: boolean;
compact?: boolean | null;
}; };
+71 -80
View File
@@ -16,7 +16,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
?? "" ?? ""
); );
const [color, setColor] = useState<`${Browser.tabGroups.Color}` | undefined | "pinned">( const [color, setColor] = useState<chrome.tabGroups.ColorEnum | 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")
@@ -24,13 +24,6 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
const cls = useStyles_EditDialog(); const cls = useStyles_EditDialog();
const colorCls = useGroupColors(); const colorCls = useGroupColors();
const horizontalNavigationAttributes = fui.useArrowNavigationGroup({ axis: "horizontal" });
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
{
e.preventDefault();
handleSave();
};
const handleSave = () => const handleSave = () =>
{ {
@@ -65,80 +58,78 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
return ( return (
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }> <fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
<form onSubmit={ onSubmit }> <fui.DialogBody>
<fui.DialogBody> <fui.DialogTitle>
<fui.DialogTitle> {
{ props.type === "collection" ?
props.type === "collection" ? i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) : i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`) }
} </fui.DialogTitle>
</fui.DialogTitle>
<fui.DialogContent> <fui.DialogContent>
<div className={ cls.content }> <form className={ cls.content }>
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }> <fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
<fui.Input <fui.Input
contentBefore={ <Rename20Regular /> } contentBefore={ <Rename20Regular /> }
disabled={ color === "pinned" } disabled={ color === "pinned" }
placeholder={ placeholder={
props.type === "collection" ? getCollectionTitle(props.collection, true) : "" props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
} }
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={ i18n.t("dialogs.edit.color") }> <fui.Field label="Color">
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }> <div className={ cls.colorPicker }>
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) && { (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
<fui.ToggleButton <fui.ToggleButton
checked={ color === "pinned" } checked={ color === "pinned" }
onClick={ () => setColor("pinned") } onClick={ () => setColor("pinned") }
icon={ <Pin20Filled /> } icon={ <Pin20Filled /> }
shape="circular" shape="circular"
> >
{ i18n.t("groups.pinned") } { i18n.t("groups.pinned") }
</fui.ToggleButton> </fui.ToggleButton>
} }
{ props.type === "collection" && { props.type === "collection" &&
<fui.ToggleButton <fui.ToggleButton
checked={ color === undefined } checked={ color === undefined }
onClick={ () => setColor(undefined) } onClick={ () => setColor(undefined) }
icon={ <CircleOff20Regular /> } icon={ <CircleOff20Regular /> }
shape="circular" shape="circular"
> >
{ i18n.t("colors.none") } { i18n.t("colors.none") }
</fui.ToggleButton> </fui.ToggleButton>
} }
{ Object.keys(colorCls).map(i => { Object.keys(colorCls).map(i =>
<fui.ToggleButton <fui.ToggleButton
checked={ color === i } checked={ color === i }
onClick={ () => setColor(i as `${Browser.tabGroups.Color}`) } onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
className={ fui.mergeClasses(cls.colorButton, colorCls[i as `${Browser.tabGroups.Color}`]) } className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
icon={ { icon={ {
className: cls.colorButton_icon, className: cls.colorButton_icon,
children: <Circle20Filled /> children: <Circle20Filled />
} } } }
key={ i } key={ i }
shape="circular" shape="circular"
> >
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) } { i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
</fui.ToggleButton> </fui.ToggleButton>
) } ) }
</div> </div>
</fui.Field> </fui.Field>
</div> </form>
</fui.DialogContent> </fui.DialogContent>
<fui.DialogActions> <fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement> <fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" as="button" type="submit">{ i18n.t("common.actions.save") }</fui.Button> <fui.Button appearance="primary" onClick={ handleSave }>{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger> </fui.DialogTrigger>
<fui.DialogTrigger disableButtonEnhancement> <fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button> <fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
</fui.DialogTrigger> </fui.DialogTrigger>
</fui.DialogActions> </fui.DialogActions>
</fui.DialogBody> </fui.DialogBody>
</form>
</fui.DialogSurface> </fui.DialogSurface>
); );
} }
@@ -99,23 +99,12 @@ export const useStyles_GroupView = makeStyles({
display: "flex", display: "flex",
columnGap: tokens.spacingHorizontalS, columnGap: tokens.spacingHorizontalS,
rowGap: tokens.spacingHorizontalSNudge, rowGap: tokens.spacingHorizontalSNudge,
height: "100%", height: "100%"
position: "relative"
}, },
verticalList: verticalList:
{ {
flexFlow: "column" flexFlow: "column"
}, },
verticalListCollapsed:
{
maxHeight: "136px",
overflow: "clip"
},
horizontalListCollapsed:
{
maxWidth: "400px",
overflow: "clip"
},
listContainer: listContainer:
{ {
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`, padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`,
+3 -12
View File
@@ -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, collectionId }: GroupViewProps): ReactElement export default function GroupView({ group, indices, dragOverlay }: GroupViewProps): ReactElement
{ {
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars"); const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
const { tilesView } = useCollections(); const { tilesView } = useCollections();
@@ -88,22 +88,14 @@ export default function GroupView({ group, indices, dragOverlay, collectionId }:
<Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong> <Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong>
</div> </div>
: :
<div <div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
className={ mergeClasses(
cls.list,
!tilesView && cls.verticalList,
((active?.item.type === "group" && active?.indices[0] === indices[0]) || dragOverlay) && (tilesView ? cls.horizontalListCollapsed : cls.verticalListCollapsed)
) }
>
<SortableContext <SortableContext
items={ group.items.map((_, index) => [...indices, index].join("/")) } items={ group.items.map((_, index) => [...indices, index].join("/")) }
disabled={ disableSorting } disabled={ disableSorting }
strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy } strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy }
> >
{ group.items.map((i, index) => { group.items.map((i, index) =>
<TabView <TabView key={ index } tab={ i } indices={ [...indices, index] } />
key={ index } tab={ i } indices={ [...indices, index] }
collectionId={ collectionId } />
) } ) }
</SortableContext> </SortableContext>
</div> </div>
@@ -119,5 +111,4 @@ export type GroupViewProps =
group: GroupItem; group: GroupItem;
indices: number[]; indices: number[];
dragOverlay?: boolean; dragOverlay?: boolean;
collectionId: number;
}; };
@@ -1,69 +0,0 @@
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;
};
@@ -1,48 +0,0 @@
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;
};
@@ -8,7 +8,6 @@ export const useStyles_TabView = makeStyles({
width: "160px", width: "160px",
height: "120px", height: "120px",
flexShrink: 0,
marginBottom: tokens.spacingVerticalSNudge, marginBottom: tokens.spacingVerticalSNudge,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`, border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`,
+15 -32
View File
@@ -4,17 +4,16 @@ 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 { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels"; import { TabItem } from "@/models/CollectionModels";
import { Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components"; import { Button, 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, collectionId }: TabViewProps): ReactElement export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement
{ {
const { removeItem, graphics, tilesView, collections, updateCollection } = useCollections(); const { removeItem, graphics, tilesView } = useCollections();
const { collection } = useContext<CollectionContextType>(CollectionContext); const { collection } = useContext<CollectionContextType>(CollectionContext);
const { const {
setNodeRef, setActivatorNodeRef, setNodeRef, setActivatorNodeRef,
@@ -27,8 +26,11 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
const cls = useStyles_TabView(); const cls = useStyles_TabView();
const handleDelete = (): void => const handleDelete: MouseEventHandler<HTMLButtonElement> = (args) =>
{ {
args.preventDefault();
args.stopPropagation();
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)]; const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
if (deletePrompt) if (deletePrompt)
@@ -43,26 +45,6 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
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();
@@ -109,10 +91,12 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
</Caption1> </Caption1>
</Tooltip> </Tooltip>
<TabMoreButton <Tooltip relationship="label" content={ i18n.t("tabs.delete") }>
className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) } <Button
onEdit={ handleEdit } className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) }
onDelete={ handleDelete } /> appearance="subtle" icon={ <Dismiss20Regular /> }
onClick={ handleDelete } />
</Tooltip>
</div> </div>
</Link> </Link>
); );
@@ -123,5 +107,4 @@ export type TabViewProps =
tab: TabItem; tab: TabItem;
indices: number[]; indices: number[];
dragOverlay?: boolean; dragOverlay?: boolean;
collectionId: number;
}; };
@@ -1,20 +1,20 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle"; import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import useSettings from "@/hooks/useSettings"; import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels"; import { GroupItem, 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, EyeOff16Regular } from "@fluentui/react-icons"; import { Add20Filled, Add20Regular, bundleIcon } 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";
import OpenCollectionButton from "./OpenCollectionButton"; import OpenCollectionButton from "./OpenCollectionButton";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification"; import saveTabsToCollection from "@/utils/saveTabsToCollection";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
{ {
const [contextOpen, setContextOpen] = useState<boolean>(false); const [contextOpen, setContextOpen] = useState<boolean>(false);
const [listLocation] = useSettings("listLocation"); const [listLocation] = useSettings("listLocation");
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned"; const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
const { updateCollection } = useCollections(); const { updateCollection } = useCollections();
const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext); const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext);
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars"); const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
@@ -23,16 +23,10 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
const handleAddSelected = async () => const handleAddSelected = async () =>
{ {
const [newTabs, skipCount] = await getTabsToSaveAsync(true); const newTabs: (TabItem | GroupItem)[] = isTab ?
(await saveTabsToCollection(false)).items :
if (newTabs.length > 0) await getSelectedTabs();
await updateCollection({ updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collection.timestamp);
...collection,
items: [...collection.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
}, collection.timestamp);
if (skipCount > 0)
await sendPartialSaveNotification();
}; };
const cls = useStyles(); const cls = useStyles();
@@ -45,12 +39,9 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
content={ getCollectionTitle(collection) } content={ getCollectionTitle(collection) }
positioning="above-start" positioning="above-start"
> >
<div className={ cls.titleContainer }> <Subtitle2 truncate wrap={ false } className={ cls.titleText }>
{ collection.hidden && <EyeOff16Regular /> } { getCollectionTitle(collection) }
<Subtitle2 truncate wrap={ false } className={ cls.titleText }> </Subtitle2>
{ getCollectionTitle(collection) }
</Subtitle2>
</div>
</Tooltip> </Tooltip>
<Caption1> <Caption1>
@@ -68,7 +59,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
> >
{ tabCount < 1 ? { tabCount < 1 ?
<Button icon={ <AddIcon /> } appearance="subtle" onClick={ handleAddSelected }> <Button icon={ <AddIcon /> } appearance="subtle" onClick={ handleAddSelected }>
{ isTabView ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") } { isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
</Button> </Button>
: :
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } /> <OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
@@ -115,11 +106,5 @@ const useStyles = makeStyles({
showToolbar: showToolbar:
{ {
display: "flex" display: "flex"
},
titleContainer:
{
display: "flex",
alignItems: "center",
gap: tokens.spacingHorizontalS
} }
}); });
@@ -22,8 +22,6 @@ 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();
@@ -41,11 +39,6 @@ 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
@@ -89,9 +82,6 @@ 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>
@@ -3,21 +3,20 @@ import EditDialog from "@/entrypoints/sidepanel/components/EditDialog";
import CollectionContext, { CollectionContextType } from "@/entrypoints/sidepanel/contexts/CollectionContext"; import CollectionContext, { CollectionContextType } from "@/entrypoints/sidepanel/contexts/CollectionContext";
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider"; import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
import GroupContext, { GroupContextType } from "@/entrypoints/sidepanel/contexts/GroupContext"; import GroupContext, { GroupContextType } from "@/entrypoints/sidepanel/contexts/GroupContext";
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import { useDangerStyles } from "@/hooks/useDangerStyles"; import { useDangerStyles } from "@/hooks/useDangerStyles";
import useSettings from "@/hooks/useSettings"; import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels"; import { TabItem } from "@/models/CollectionModels";
import { sendMessage } from "@/utils/messaging";
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components"; import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons"; import * as ic from "@fluentui/react-icons";
import { ReactElement } from "react"; import { ReactElement } from "react";
import { openGroup } from "../../utils/opener"; import { openGroup } from "../../utils/opener";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync"; import saveTabsToCollection from "@/utils/saveTabsToCollection";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
export default function GroupMoreMenu(): ReactElement export default function GroupMoreMenu(): ReactElement
{ {
const [listLocation] = useSettings("listLocation"); const [listLocation] = useSettings("listLocation");
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned"; const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
const { group, indices } = useContext<GroupContextType>(GroupContext); const { group, indices } = useContext<GroupContextType>(GroupContext);
const { hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext); const { hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
const [deletePrompt] = useSettings("deletePrompt"); const [deletePrompt] = useSettings("deletePrompt");
@@ -57,26 +56,12 @@ export default function GroupMoreMenu(): ReactElement
onSave={ item => updateGroup(item, collection.timestamp, indices[1]) } /> onSave={ item => updateGroup(item, collection.timestamp, indices[1]) } />
); );
const openGroupInNewWindow = () =>
{
if (import.meta.env.FIREFOX && listLocation === "popup")
sendMessage("openGroup", { group, newWindow: true });
else
openGroup(group, true);
};
const handleAddSelected = async () => const handleAddSelected = async () =>
{ {
const [newTabs, skipCount] = await getTabsToSaveAsync(true); const newTabs: TabItem[] = isTab ?
(await saveTabsToCollection(false)).items.flatMap(i => i.type === "tab" ? i : i.items) :
if (newTabs.length > 0) await getSelectedTabs();
await updateGroup({ updateGroup({ ...group, items: [...group.items, ...newTabs] }, collection.timestamp, indices[1]);
...group,
items: [...group.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
}, collection.timestamp, indices[1]);
if (skipCount > 0)
await sendPartialSaveNotification();
}; };
return ( return (
@@ -90,13 +75,13 @@ export default function GroupMoreMenu(): ReactElement
<MenuPopover> <MenuPopover>
<MenuList> <MenuList>
{ group.items.length > 0 && { group.items.length > 0 &&
<MenuItem icon={ <NewWindowIcon /> } onClick={ openGroupInNewWindow }> <MenuItem icon={ <NewWindowIcon /> } onClick={ () => openGroup(group, true) }>
{ i18n.t("groups.menu.new_window") } { i18n.t("groups.menu.new_window") }
</MenuItem> </MenuItem>
} }
<MenuItem icon={ <AddIcon /> } onClick={ handleAddSelected }> <MenuItem icon={ <AddIcon /> } onClick={ handleAddSelected }>
{ isTabView ? i18n.t("groups.menu.add_all") : i18n.t("groups.menu.add_selected") } { isTab ? i18n.t("groups.menu.add_all") : i18n.t("groups.menu.add_selected") }
</MenuItem> </MenuItem>
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }> <MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
@@ -1,7 +1,6 @@
import { useDialog } from "@/contexts/DialogProvider"; import { useDialog } from "@/contexts/DialogProvider";
import useSettings from "@/hooks/useSettings"; import useSettings from "@/hooks/useSettings";
import browserLocaleKey from "@/utils/browserLocaleKey"; import browserLocaleKey from "@/utils/browserLocaleKey";
import { sendMessage } from "@/utils/messaging";
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components"; import { Menu, MenuButtonProps, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons"; import * as ic from "@fluentui/react-icons";
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext"; import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
@@ -11,7 +10,6 @@ import { openCollection } from "../../utils/opener";
export default function OpenCollectionButton({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement export default function OpenCollectionButton({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement
{ {
const [defaultAction] = useSettings("defaultRestoreAction"); const [defaultAction] = useSettings("defaultRestoreAction");
const [listLocation] = useSettings("listLocation");
const { removeItem } = useCollections(); const { removeItem } = useCollections();
const dialog = useDialog(); const dialog = useDialog();
const { collection } = useContext<CollectionContextType>(CollectionContext); const { collection } = useContext<CollectionContextType>(CollectionContext);
@@ -24,12 +22,7 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
const handleIncognito = async () => const handleIncognito = async () =>
{ {
if (await browser.extension.isAllowedIncognitoAccess()) if (await browser.extension.isAllowedIncognitoAccess())
{ openCollection(collection, "incognito");
if (import.meta.env.FIREFOX && listLocation === "popup")
sendMessage("openCollection", { collection, targetWindow: "incognito" });
else
openCollection(collection, "incognito");
}
else else
dialog.pushPrompt({ dialog.pushPrompt({
title: i18n.t("collections.incognito_check.title"), title: i18n.t("collections.incognito_check.title"),
@@ -52,9 +45,7 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
}; };
const handleOpen = (mode: "current" | "new") => const handleOpen = (mode: "current" | "new") =>
import.meta.env.FIREFOX && listLocation === "popup" && mode === "new" ? () => openCollection(collection, mode);
() => sendMessage("openCollection", { collection, targetWindow: "new" }) :
() => openCollection(collection, mode);
const handleRestore = async () => const handleRestore = async () =>
{ {
@@ -43,12 +43,12 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
sendMessage("refreshCollections", undefined); sendMessage("refreshCollections", undefined);
}; };
const addCollection = async (collection: CollectionItem): Promise<void> => const addCollection = (collection: CollectionItem): void =>
{ {
await updateStorage([collection, ...collections]); updateStorage([collection, ...collections]);
}; };
const removeItem = async (...indices: number[]): Promise<void> => const removeItem = (...indices: number[]): void =>
{ {
const collectionIndex: number = collections.findIndex(i => i.timestamp === indices[0]); const collectionIndex: number = collections.findIndex(i => i.timestamp === indices[0]);
@@ -59,34 +59,34 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
else else
collections.splice(collectionIndex, 1); collections.splice(collectionIndex, 1);
await updateStorage(collections); updateStorage(collections);
}; };
const updateCollections = async (collectionList: CollectionItem[]): Promise<void> => const updateCollections = (collectionList: CollectionItem[]): void =>
{ {
await updateStorage(collectionList); updateStorage(collectionList);
}; };
const updateCollection = async (collection: CollectionItem, id: number): Promise<void> => const updateCollection = (collection: CollectionItem, id: number): void =>
{ {
const index: number = collections.findIndex(i => i.timestamp === id); const index: number = collections.findIndex(i => i.timestamp === id);
collections[index] = collection; collections[index] = collection;
await updateStorage(collections); updateStorage(collections);
}; };
const updateGroup = async (group: GroupItem, collectionId: number, groupIndex: number): Promise<void> => const updateGroup = (group: GroupItem, collectionId: number, groupIndex: number): void =>
{ {
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId); const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
collections[collectionIndex].items[groupIndex] = group; collections[collectionIndex].items[groupIndex] = group;
await updateStorage(collections); updateStorage(collections);
}; };
const ungroup = async (collectionId: number, groupIndex: number): Promise<void> => const ungroup = (collectionId: number, groupIndex: number): void =>
{ {
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId); const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
const group = collections[collectionIndex].items[groupIndex] as GroupItem; const group = collections[collectionIndex].items[groupIndex] as GroupItem;
collections[collectionIndex].items.splice(groupIndex, 1, ...group.items); collections[collectionIndex].items.splice(groupIndex, 1, ...group.items);
await updateStorage(collections); updateStorage(collections);
}; };
return ( return (
@@ -110,12 +110,12 @@ export type CollectionsContextType =
tilesView: boolean; tilesView: boolean;
refreshCollections: () => Promise<void>; refreshCollections: () => Promise<void>;
addCollection: (collection: CollectionItem) => Promise<void>; addCollection: (collection: CollectionItem) => void;
updateCollections: (collections: CollectionItem[]) => Promise<void>; updateCollections: (collections: CollectionItem[]) => void;
updateCollection: (collection: CollectionItem, id: number) => Promise<void>; updateCollection: (collection: CollectionItem, id: number) => void;
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => Promise<void>; updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => void;
ungroup: (collectionId: number, groupIndex: number) => Promise<void>; ungroup: (collectionId: number, groupIndex: number) => void;
removeItem: (...indices: number[]) => Promise<void>; removeItem: (...indices: number[]) => void;
}; };
@@ -51,9 +51,5 @@ export const useStyles_CollectionListView = makeStyles({
{ {
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))" gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
} }
},
compactList:
{
alignItems: "baseline"
} }
}); });
@@ -18,7 +18,6 @@ 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";
@@ -30,19 +29,17 @@ 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: 150, tolerance: 20 } }), useSensor(MouseSensor, { activationConstraint: { delay: 10, 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, showHidden }), sortMode), () => sortCollections(filterCollections(collections, { query, colors }), sortMode),
[query, colors, sortMode, collections, showHidden] [query, colors, sortMode, collections]
); );
const cls = useStyles_CollectionListView(); const cls = useStyles_CollectionListView();
@@ -51,13 +48,6 @@ 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 =>
@@ -96,9 +86,8 @@ 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={ updateFilter } filter={ colors } onFilterChange={ setColors }
sort={ sortMode } onSortChange={ setSortMode } sort={ sortMode } onSortChange={ setSortMode }
showHidden={ showHidden }
onReset={ resetFilter } /> onReset={ resetFilter } />
<CtaMessage className={ cls.msgBar } /> <CtaMessage className={ cls.msgBar } />
@@ -115,20 +104,19 @@ export default function CollectionListView(): ReactElement
</Button> </Button>
</div> </div>
: :
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView, !!(!tilesView && compactView) && cls.compactList) }> <section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView) }>
<DndContext <DndContext
sensors={ sensors } sensors={ sensors }
collisionDetection={ collisionDetector(!tilesView) } collisionDetection={ collisionDetector(!tilesView) }
onDragStart={ handleDragStart } onDragStart={ handleDragStart }
onDragEnd={ handleDragEnd } onDragEnd={ handleDragEnd }
modifiers={ [snapHandleToCursor] }
> >
<SortableContext <SortableContext
items={ resultList.map((_, index) => index.toString()) } items={ resultList.map((_, index) => index.toString()) }
strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy } strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy }
> >
{ resultList.map((collection, index) => { resultList.map((collection, index) =>
<CollectionView key={ index } collection={ collection } index={ index } compact={ compactView } /> <CollectionView key={ index } collection={ collection } index={ index } />
) } ) }
</SortableContext> </SortableContext>
@@ -145,9 +133,9 @@ export default function CollectionListView(): ReactElement
} } } }
> >
{ active.item.type === "group" ? { active.item.type === "group" ?
<GroupView group={ active.item } indices={ [-1] } collectionId={ -1 } dragOverlay /> <GroupView group={ active.item } indices={ [-1] } dragOverlay />
: :
<TabView tab={ active.item } indices={ [-1] } collectionId={ -1 } dragOverlay /> <TabView tab={ active.item } indices={ [-1] } dragOverlay />
} }
</CollectionContext.Provider> </CollectionContext.Provider>
: :
@@ -3,48 +3,32 @@ 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, showHidden }: FilterCollectionsButtonProps): React.ReactElement export default function FilterCollectionsButton({ value, onChange }: FilterCollectionsButtonProps): React.ReactElement
{ {
const cls = useStyles(); const cls = useStyles();
const colorCls = useGroupColors(); const colorCls = useGroupColors();
const FilterIcon = ic.bundleIcon(ic.Filter20Filled, ic.Filter20Regular); const ColorFilterIcon = ic.bundleIcon(ic.Color20Filled, ic.Color20Regular);
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={ values } checkedValues={ !value || value.length < 1 ? { default: ["any"] } : { colors: value } }
onCheckedValueChange={ onCheckedValueChange } onCheckedValueChange={ (_, e) =>
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={ <FilterIcon /> } /> <fui.Button appearance="subtle" icon={ <ColorFilterIcon /> } />
</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>
@@ -60,11 +44,11 @@ export default function FilterCollectionsButton({ value, onChange, showHidden }:
<ColorIcon <ColorIcon
className={ fui.mergeClasses( className={ fui.mergeClasses(
cls.colorIcon, cls.colorIcon,
colorCls[i as `${Browser.tabGroups.Color}`] colorCls[i as chrome.tabGroups.ColorEnum]
) } /> ) } />
} }
> >
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) } { i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
</fui.MenuItemCheckbox> </fui.MenuItemCheckbox>
) } ) }
</fui.MenuList> </fui.MenuList>
@@ -76,8 +60,7 @@ export default function FilterCollectionsButton({ value, onChange, showHidden }:
export type FilterCollectionsButtonProps = export type FilterCollectionsButtonProps =
{ {
value?: CollectionFilterType["colors"]; value?: CollectionFilterType["colors"];
showHidden?: boolean; onChange?: (value: CollectionFilterType["colors"]) => void;
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 } showHidden={ props.showHidden } /> <FilterCollectionsButton value={ props.filter } onChange={ props.onFilterChange } />
<SortCollectionsButton value={ props.sort } onChange={ props.onSortChange } /> <SortCollectionsButton value={ props.sort } onChange={ props.onSortChange } />
</> </>
} /> } />
@@ -37,9 +37,8 @@ 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"], showHidden: boolean) => void; onFilterChange?: (filter: CollectionFilterType["colors"]) => void;
sort?: CollectionSortMode; sort?: CollectionSortMode;
showHidden?: boolean;
onSortChange?: (sort: CollectionSortMode) => void; onSortChange?: (sort: CollectionSortMode) => void;
onReset?: () => void; onReset?: () => void;
}; };
@@ -1,8 +1,7 @@
import resolveConflict from "@/features/collectionStorage/utils/resolveConflict"; import resolveConflict from "@/features/collectionStorage/utils/resolveConflict";
import { Button, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components"; import { Button, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
import { ArrowDownload20Regular, ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons"; import { ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons";
import { useCollections } from "../../../contexts/CollectionsProvider"; import { useCollections } from "../../../contexts/CollectionsProvider";
import exportData from "@/entrypoints/options/utils/exportData";
export default function CloudIssueMessages(props: MessageBarProps): React.ReactElement export default function CloudIssueMessages(props: MessageBarProps): React.ReactElement
{ {
@@ -37,9 +36,6 @@ export default function CloudIssueMessages(props: MessageBarProps): React.ReactE
{ i18n.t("merge_conflict_message.message") } { i18n.t("merge_conflict_message.message") }
</MessageBarBody> </MessageBarBody>
<MessageBarActions> <MessageBarActions>
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
{ i18n.t("options_page.storage.export") }
</Button>
<Button icon={ <ArrowUpload20Regular /> } onClick={ () => overrideStorageWith("local") }> <Button icon={ <ArrowUpload20Regular /> } onClick={ () => overrideStorageWith("local") }>
{ i18n.t("merge_conflict_message.accept_local") } { i18n.t("merge_conflict_message.accept_local") }
</Button> </Button>
@@ -1,7 +1,7 @@
import useStorageInfo from "@/hooks/useStorageInfo"; import useStorageInfo from "@/hooks/useStorageInfo";
import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components"; import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
export default function StorageCapacityIssueMessage(props: MessageBarProps): React.ReactElement export default function StorageCapacityIssueMessage(props: MessageBarProps): JSX.Element
{ {
const { usedStorageRatio } = useStorageInfo(); const { usedStorageRatio } = useStorageInfo();
@@ -1,11 +1,6 @@
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider"; import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
import { track } from "@/features/analytics";
import useSettings, { SettingsValue } from "@/hooks/useSettings"; import useSettings, { SettingsValue } from "@/hooks/useSettings";
import { CollectionItem } from "@/models/CollectionModels"; import saveTabsToCollection from "@/utils/saveTabsToCollection";
import { closeTabsAsync } from "@/utils/closeTabsAsync";
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
import watchTabSelection from "@/utils/watchTabSelection"; import watchTabSelection from "@/utils/watchTabSelection";
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components"; import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons"; import * as ic from "@fluentui/react-icons";
@@ -19,26 +14,8 @@ export default function ActionButton(): ReactElement
const handleAction = async (primary: boolean) => const handleAction = async (primary: boolean) =>
{ {
const [tabs, skipCount] = await getTabsToSaveAsync(); const colection = await saveTabsToCollection(primary === (defaultAction === "set_aside"));
addCollection(colection);
if (tabs.length < 1)
{
await sendPartialSaveNotification();
return;
}
const collection: CollectionItem = await createCollectionFromTabs(tabs);
await addCollection(collection);
if (skipCount > 0)
await sendPartialSaveNotification();
const closeTabs: boolean = primary === (defaultAction === "set_aside");
if (closeTabs)
await closeTabsAsync(tabs);
track(closeTabs ? "set_aside" : "save");
}; };
useEffect(() => useEffect(() =>
@@ -11,32 +11,18 @@ 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 GridIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular); const ViewIcon: 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={ checkedValues } onCheckedValueChange={ onCheckedValueChange } checkedValues={ { tilesView: tilesView ? ["true"] : [] } }
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>
@@ -50,12 +36,9 @@ 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="view" value="tiles" icon={ <GridIcon /> }> <fui.MenuItemCheckbox name="tilesView" value="true" icon={ <ViewIcon /> }>
{ 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 />
+2 -8
View File
@@ -1,6 +1,5 @@
import App from "@/App.tsx"; import App from "@/App.tsx";
import "@/assets/global.css"; import "@/assets/global.css";
import { trackPage } from "@/features/analytics";
import { useLocalMigration } from "@/features/migration"; import { useLocalMigration } from "@/features/migration";
import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog"; import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog";
import { Divider, makeStyles } from "@fluentui/react-components"; import { Divider, makeStyles } from "@fluentui/react-components";
@@ -8,8 +7,6 @@ import ReactDOM from "react-dom/client";
import CollectionsProvider from "./contexts/CollectionsProvider"; import CollectionsProvider from "./contexts/CollectionsProvider";
import CollectionListView from "./layouts/collections/CollectionListView"; import CollectionListView from "./layouts/collections/CollectionListView";
import Header from "./layouts/header/Header"; import Header from "./layouts/header/Header";
import { useSettingsReviewDialog } from "@/features/settingsReview";
import useDialogTrain from "@/hooks/useDialogTrain";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<App> <App>
@@ -18,17 +15,14 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
); );
document.title = i18n.t("manifest.name"); document.title = i18n.t("manifest.name");
trackPage("collection_list"); analytics.page("collection_list");
function MainPage(): React.ReactElement function MainPage(): React.ReactElement
{ {
const cls = useStyles(); const cls = useStyles();
useLocalMigration(); useLocalMigration();
useDialogTrain( useWelcomeDialog();
useWelcomeDialog,
useSettingsReviewDialog
);
return ( return (
<CollectionsProvider> <CollectionsProvider>
@@ -33,11 +33,9 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
if (activeItem.item.type === "collection") if (activeItem.item.type === "collection")
{ {
// If we drag a collection, we should ignore other items, like tabs or groups
if (droppableItem.item.type !== "collection") if (droppableItem.item.type !== "collection")
continue; continue;
// Using distance between centers
value = distanceBetween(centerOfRectangle(rect), centerRect); value = distanceBetween(centerOfRectangle(rect), centerRect);
collisions.push({ id, data: { droppableContainer, value } }); collisions.push({ id, data: { droppableContainer, value } });
continue; continue;
@@ -46,20 +44,14 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
const intersectionRatio: number = getIntersectionRatio(rect, collisionRect); const intersectionRatio: number = getIntersectionRatio(rect, collisionRect);
const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect); const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect);
// Dragging a tab or a group over a collection
if (droppableItem.item.type === "collection") if (droppableItem.item.type === "collection")
{ {
// Ignoring collection, if the tab or the group is inside that collection
if (activeItem.indices.length === 2 && activeItem.indices[0] === droppableItem.indices[0]) if (activeItem.indices.length === 2 && activeItem.indices[0] === droppableItem.indices[0])
continue; continue;
// Ignoring collection if we're dragging a tab or a group that doesn't belong to the collection, if (intersectionCoefficient < 0.7 && activeItem.item.type === "tab")
// but intersection ratio is less than 0.7
if (intersectionCoefficient < 0.7)
continue; continue;
// If we're dragging a tab, that's inside a group that belongs to the collection,
// we substract the group's intersection from the collection's one
if (activeItem.indices.length === 3 && activeItem.indices[0] === droppableItem.indices[0]) if (activeItem.indices.length === 3 && activeItem.indices[0] === droppableItem.indices[0])
{ {
const [collectionId, groupId] = activeItem.indices; const [collectionId, groupId] = activeItem.indices;
@@ -70,23 +62,16 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
value = 1 / (intersectionRatio - getIntersectionRatio(groupRect, collisionRect)); value = 1 / (intersectionRatio - getIntersectionRatio(groupRect, collisionRect));
} }
// Otherwise, use intersection ratio
// At this point we're dragging either:
// - a group, that doesn't belong to the collection
// - a tab, that either belongs to the collection's group, or has intersection coefficient >= .7
else else
{ {
value = 2 / intersectionRatio; value = 1 / intersectionRatio;
} }
} }
// If we're dragging a tab or a group over another group's dropzone
else if (droppableItem.item.type === "group" && (id as string).endsWith("_dropzone")) else if (droppableItem.item.type === "group" && (id as string).endsWith("_dropzone"))
{ {
// Ignore, if we're dragging a group
if (activeItem.item.type === "group") if (activeItem.item.type === "group")
continue; continue;
// Ignore, if we're dragging a tab, that's inside the group
if ( if (
activeItem.indices.length === 3 && activeItem.indices.length === 3 &&
activeItem.indices[0] === droppableItem.indices[0] && activeItem.indices[0] === droppableItem.indices[0] &&
@@ -94,15 +79,11 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
) )
continue; continue;
// Ignore, if coefficient is less than .5
// (at this point we're dragging a tab, that's outside of the group's dropzone)
if (intersectionCoefficient < 0.5) if (intersectionCoefficient < 0.5)
continue; continue;
// Use intersection between the tab and the group's dropzone
value = 1 / intersectionRatio; value = 1 / intersectionRatio;
} }
// We're dragging a group or a tab over its sibling
else if (activeItem.indices.length === droppableItem.indices.length) else if (activeItem.indices.length === droppableItem.indices.length)
{ {
if (activeItem.indices[0] !== droppableItem.indices[0]) if (activeItem.indices[0] !== droppableItem.indices[0])
@@ -111,22 +92,9 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
if (activeItem.indices.length === 3 && activeItem.indices[1] !== droppableItem.indices[1]) if (activeItem.indices.length === 3 && activeItem.indices[1] !== droppableItem.indices[1])
continue; continue;
// Ignore pinned groups
if (droppableItem.item.type === "group" && droppableItem.item.pinned === true) if (droppableItem.item.type === "group" && droppableItem.item.pinned === true)
continue; continue;
const collectionRect: ClientRect | undefined = droppableRects.get(activeItem.indices[0].toString());
if (!collectionRect)
continue;
const collectionIntersectionRatio: number = getIntersectionRatio(collectionRect, collisionRect);
const collectionIntersectionCoefficient: number = collectionIntersectionRatio / getMaxIntersectionRatio(collectionRect, collisionRect);
// Ignore if we are outside of the home collection
if (collectionIntersectionCoefficient < 0.7)
continue;
if (activeItem.item.type === "tab" && droppableItem.item.type === "tab") if (activeItem.item.type === "tab" && droppableItem.item.type === "tab")
{ {
value = distanceBetween(centerOfRectangle(rect), centerRect); value = distanceBetween(centerOfRectangle(rect), centerRect);
@@ -1,34 +0,0 @@
import { Modifier } from "@dnd-kit/core";
import { Coordinates, getEventCoordinates } from "@dnd-kit/utilities";
import { DndItem } from "../../hooks/useDndItem";
export const snapHandleToCursor: Modifier = ({
activatorEvent,
draggingNodeRect,
transform,
active
}) =>
{
if (draggingNodeRect && activatorEvent)
{
const activeItem: DndItem | undefined = active?.data.current as DndItem;
const activatorCoordinates: Coordinates | null = getEventCoordinates(activatorEvent);
if (!activatorCoordinates)
return transform;
const initX: number = activatorCoordinates.x - draggingNodeRect.left;
const initY: number = activatorCoordinates.y - draggingNodeRect.top;
const offsetX: number = activeItem?.item.type === "group" ? 24 : draggingNodeRect.height / 2;
const offsetY: number = activeItem?.item.type === "group" ? 20 : draggingNodeRect.height / 2;
return {
...transform,
x: transform.x + initX - offsetX,
y: transform.y + initY - offsetY
};
}
return transform;
};
@@ -1,21 +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 } from "wxt/browser";
import { getCollectionTitle } from "./getCollectionTitle"; import { getCollectionTitle } from "./getCollectionTitle";
export default async function exportCollectionToBookmarks(collection: CollectionItem) export default async function exportCollectionToBookmarks(collection: CollectionItem)
{ {
const permissions: Browser.permissions.Permissions = await browser.permissions.getAll(); const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
if (!permissions.permissions?.includes("bookmarks"))
{
const granted: boolean = await browser.permissions.request({ permissions: ["bookmarks"] });
if (!granted)
return;
}
const rootFolder: Browser.bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
title: getCollectionTitle(collection) title: getCollectionTitle(collection)
}); });
@@ -41,8 +31,6 @@ export default async function exportCollectionToBookmarks(collection: Collection
} }
} }
track("bookmarks_saved");
await sendNotification({ await sendNotification({
title: i18n.t("notifications.bookmark_saved.title"), title: i18n.t("notifications.bookmark_saved.title"),
message: i18n.t("notifications.bookmark_saved.message"), message: i18n.t("notifications.bookmark_saved.message"),
@@ -9,16 +9,13 @@ export default function filterCollections(
if (!collections || collections.length < 1) if (!collections || collections.length < 1)
return []; return [];
if (!filter.query && filter.colors.length < 1 && filter.showHidden) if (!filter.query && filter.colors.length < 1)
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 ||
@@ -64,6 +61,5 @@ export default function filterCollections(
export type CollectionFilterType = export type CollectionFilterType =
{ {
query: string; query: string;
colors: (`${Browser.tabGroups.Color}` | "none")[]; colors: (chrome.tabGroups.ColorEnum | "none")[];
showHidden: boolean;
}; };
@@ -1,9 +1,10 @@
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: Browser.tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true }); let tabs: 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 =>
+10 -9
View File
@@ -1,6 +1,7 @@
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>
{ {
@@ -54,7 +55,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: Browser.tabs.Tab[] = await Promise.all(group.items.map(async i => const tabs: 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)
)); ));
@@ -62,21 +63,21 @@ async function createGroup(group: GroupItem, windowId: number, discard?: boolean
if (group.pinned === true) if (group.pinned === true)
return; return;
const groupId: number = await browser.tabs.group({ const groupId: number = await chrome.tabs.group({
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!) as [number, ...number[]], tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!),
createProperties: { windowId } createProperties: { windowId }
}); });
await browser.tabGroups.update(groupId, { await chrome.tabGroups.update(groupId, {
title: group.title, title: group.title,
color: group.color color: group.color
}); });
} }
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Browser.windows.CreateData): Promise<void> async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Windows.CreateCreateDataType): Promise<void>
{ {
const currentWindow: Browser.windows.Window = windowProps ? const currentWindow: 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!;
@@ -89,7 +90,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<Browser.tabs.Tab> async function createTab(url: string, windowId: number, discard: boolean, pinned?: boolean): Promise<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 });
@@ -101,7 +102,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: Browser.tabs.Tab) => const handleTabUpdated = (id: number, _: any, tab: Tabs.Tab) =>
{ {
if (id !== tabId || !tab.url) if (id !== tabId || !tab.url)
return; return;
+2 -3
View File
@@ -19,14 +19,13 @@ 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", "package-lock.json"], ignores: [".devcontainer/devcontainer.json"],
plugins: { json }, plugins: { json },
language: "json/json", language: "json/json",
extends: ["json/recommended"] extends: ["json/recommended"]
@@ -51,7 +50,7 @@ export default defineConfig([
"@stylistic/semi": ["error", "always"], "@stylistic/semi": ["error", "always"],
"@stylistic/block-spacing": ["warn", "always"], "@stylistic/block-spacing": ["warn", "always"],
"@stylistic/arrow-spacing": ["warn", { before: true, after: true }], "@stylistic/arrow-spacing": ["warn", { before: true, after: true }],
"@stylistic/indent": ["warn", "tab", { assignmentOperator: "off" }], "@stylistic/indent": ["warn", "tab"],
"@stylistic/quotes": ["error", "double"], "@stylistic/quotes": ["error", "double"],
"@stylistic/comma-spacing": ["warn"], "@stylistic/comma-spacing": ["warn"],
"@stylistic/comma-dangle": ["warn", "never"], "@stylistic/comma-dangle": ["warn", "never"],
+3 -55
View File
@@ -1,55 +1,3 @@
import { analytics } from "./utils/analytics"; export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
import analyticsPermission from "./utils/analyticsPermission"; export { default as trackError } from "./utils/trackError";
import { getUserProperties, userId } from "./utils/getUserProperties"; export { default as track } from "./utils/track";
export { analyticsPermission };
export async function track(eventName: string, eventProperties?: Record<string, string>): Promise<void>
{
try
{
if (!await analyticsPermission.getValue())
return;
analytics.track(eventName, eventProperties);
}
catch (ex)
{
console.error("Failed to send analytics event", ex);
}
}
export async function trackError(name: string, error: Error): Promise<void>
{
try
{
if (!await analyticsPermission.getValue())
return;
analytics.track(name, {
name: error.name,
message: error.message,
stack: error.stack ?? "no_stack"
});
}
catch (ex)
{
console.error("Failed to send error report", ex);
}
}
export async function trackPage(pageName: string): Promise<void>
{
try
{
if (!await analyticsPermission.getValue())
return;
analytics.identify(await userId.getValue() as string, await getUserProperties());
analytics.page(pageName);
}
catch (ex)
{
console.error("Failed to send page view", ex);
}
}
-12
View File
@@ -1,12 +0,0 @@
import { createAnalytics } from "@wxt-dev/analytics";
import { googleAnalytics4 } from "@wxt-dev/analytics/providers/google-analytics-4";
export const analytics = createAnalytics({
providers:
[
googleAnalytics4({
apiSecret: import.meta.env.WXT_GA4_API_SECRET,
measurementId: import.meta.env.WXT_GA4_MEASUREMENT_ID
})
]
});
@@ -1,74 +0,0 @@
import { Unwatch, WatchCallback } from "wxt/utils/storage";
import { analytics } from "./analytics";
const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
{
getValue: async (): Promise<boolean> =>
{
const isGranted: boolean = import.meta.env.FIREFOX
? await browser.permissions.contains({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions)
: await allowAnalytics.getValue();
analytics.setEnabled(isGranted);
return isGranted;
},
setValue: async (value: boolean) =>
{
if (!import.meta.env.FIREFOX)
{
await allowAnalytics.setValue(value);
return;
}
let result: boolean = false;
if (value)
result = await browser.permissions.request({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions);
else
result = await browser.permissions.remove({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions);
if (!result)
throw new Error("Permission request was denied");
},
watch: (cb: WatchCallback<boolean>): Unwatch =>
{
if (!import.meta.env.FIREFOX)
return allowAnalytics.watch(cb);
const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
{
// @ts-expect-error Firefox-only API
if (permissions.data_collection?.includes("technicalAndInteraction"))
{
const isGranted: boolean = await browser.permissions.contains({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions);
cb(isGranted, !isGranted);
}
};
browser.permissions.onAdded.addListener(listener);
browser.permissions.onRemoved.addListener(listener);
return (): void =>
{
browser.permissions.onAdded.removeListener(listener);
browser.permissions.onRemoved.removeListener(listener);
};
}
};
export default analyticsPermission;
const allowAnalytics = storage.defineItem<boolean>("local:analytics", {
fallback: true
});
@@ -1,30 +0,0 @@
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
import { settings } from "@/utils/settings";
export async function getUserProperties(): Promise<UserProperties>
{
const properties: UserProperties =
{
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
collection_count: (await collectionCount.getValue()).toString()
};
for (const key of Object.keys(settings))
{
const value = await settings[key as keyof typeof settings].getValue();
properties[`option_${key}`] = value.valueOf().toString();
}
return properties;
}
export const userId = storage.defineItem("local:userId", {
init: () => crypto.randomUUID()
});
export type UserProperties =
{
collection_count: string;
cloud_used: string;
[key: `option_${string}`]: string;
};
+11
View File
@@ -0,0 +1,11 @@
export default function track(eventName: string, eventProperties?: Record<string, string>): void
{
try
{
analytics.track(eventName, eventProperties);
}
catch (ex)
{
console.error("Failed to send analytics event", ex);
}
}
+15
View File
@@ -0,0 +1,15 @@
export default function trackError(name: string, error: Error): void
{
try
{
analytics.track(name, {
name: error.name,
message: error.message,
stack: error.stack ?? "no_stack"
});
}
catch (ex)
{
console.error("Failed to send error report", ex);
}
}
@@ -0,0 +1,35 @@
import { cloudDisabled, collectionCount } from "@/features/collectionStorage";
import { settings } from "@/utils/settings";
import { WxtStorageItem } from "wxt/storage";
// @ts-expect-error we don't need to implement a full storage item
const userPropertiesStorage: WxtStorageItem<Record<string, string>, any> =
{
getValue: async (): Promise<UserProperties> =>
{
console.log("userPropertiesStorage.getValue");
const properties: UserProperties =
{
cloud_used: await cloudDisabled.getValue() ? "-1" : (await browser.storage.sync.getBytesInUse() / 102400).toString(),
collection_count: (await collectionCount.getValue()).toString()
};
for (const key of Object.keys(settings))
{
const value = await settings[key as keyof typeof settings].getValue();
properties[`option_${key}`] = value.valueOf().toString();
}
return properties;
},
setValue: async () => { }
};
export default userPropertiesStorage;
export type UserProperties =
{
collection_count: string;
cloud_used: string;
[key: `option_${string}`]: string;
};
-3
View File
@@ -5,9 +5,6 @@ export { default as getCollections } from "./utils/getCollections";
export { default as resoveConflict } from "./utils/resolveConflict"; export { default as resoveConflict } from "./utils/resolveConflict";
export { default as saveCollections } from "./utils/saveCollections"; export { default as saveCollections } from "./utils/saveCollections";
export { default as setCloudStorage } from "./utils/setCloudStorage"; export { default as setCloudStorage } from "./utils/setCloudStorage";
export { default as clearGraphicsStorage } from "./utils/clearGraphics";
export { default as thumbnailCaptureEnabled } from "./utils/thumbnailCaptureEnabled";
export const collectionCount = collectionStorage.count; export const collectionCount = collectionStorage.count;
export const graphics = collectionStorage.graphics; export const graphics = collectionStorage.graphics;
@@ -1,6 +0,0 @@
import { collectionStorage } from "./collectionStorage";
export default async function clearGraphicsStorage(): Promise<void>
{
await collectionStorage.graphics.removeValue();
}
@@ -14,7 +14,7 @@ export default async function getCollectionsFromCloud(): Promise<CollectionItem[
const chunks: Record<string, string> = const chunks: Record<string, string> =
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>; await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
const data: string = decompress(Object.values(chunks).join(""), { inputEncoding: "Base64" }); const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "Base64" });
return parseCollections(data); return parseCollections(data);
} }
@@ -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 `${Browser.tabGroups.Color}`, color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
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 `${Browser.tabGroups.Color}`, color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
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,5 +1,7 @@
import { trackError } from "@/features/analytics";
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels"; import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger"; import getLogger from "@/utils/getLogger";
import sendNotification from "@/utils/sendNotification";
import { collectionStorage } from "./collectionStorage"; import { collectionStorage } from "./collectionStorage";
import saveCollectionsToCloud from "./saveCollectionsToCloud"; import saveCollectionsToCloud from "./saveCollectionsToCloud";
import saveCollectionsToLocal from "./saveCollectionsToLocal"; import saveCollectionsToLocal from "./saveCollectionsToLocal";
@@ -17,8 +19,29 @@ export default async function saveCollections(
await saveCollectionsToLocal(collections, timestamp); await saveCollectionsToLocal(collections, timestamp);
if (updateCloud && await collectionStorage.disableCloud.getValue() !== true) if (updateCloud && await collectionStorage.disableCloud.getValue() !== true)
await saveCollectionsToCloud(collections, timestamp); try
{
await saveCollectionsToCloud(collections, timestamp);
}
catch (ex)
{
logger("Failed to save cloud storage");
console.error(ex);
trackError("cloud_save_error", ex as Error);
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
await sendNotification({
title: i18n.t("notifications.error_quota_exceeded.title"),
message: i18n.t("notifications.error_quota_exceeded.message"),
icon: "/notification_icons/cloud_error.png"
});
else
await sendNotification({
title: i18n.t("notifications.error_storage_full.title"),
message: i18n.t("notifications.error_storage_full.message"),
icon: "/notification_icons/cloud_error.png"
});
}
await updateGraphics(collections, graphicsCache); await updateGraphics(collections, graphicsCache);
logger("Save complete");
}; };
@@ -1,75 +1,46 @@
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";
const logger = getLogger("saveCollectionsToCloud");
export default async function saveCollectionsToCloud(collections: CollectionItem[], timestamp: number): Promise<void> export default async function saveCollectionsToCloud(collections: CollectionItem[], timestamp: number): Promise<void>
{ {
try if (!collections || collections.length < 1)
{ {
if (!collections || collections.length < 1) await collectionStorage.chunkCount.setValue(0);
{ await browser.storage.sync.remove(getChunkKeys());
await browser.storage.sync.set({ return;
[getStorageKey(collectionStorage.chunkCount)]: 0,
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
});
await browser.storage.sync.remove(getChunkKeys());
return;
}
const data: string = compress(serializeCollections(collections), { outputEncoding: "Base64" });
const chunks: string[] = splitIntoChunks(data);
if (chunks.length > collectionStorage.maxChunkCount)
throw new Error("Data is too large to be stored in sync storage.");
// Since there's a limit for cloud write operations, we need to write all chunks in one go.
const newRecords: Record<string, string | number> =
{
[getStorageKey(collectionStorage.chunkCount)]: chunks.length,
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
};
for (let i = 0; i < chunks.length; i++)
newRecords[`c${i}`] = chunks[i];
await browser.storage.sync.set(newRecords);
if (chunks.length < collectionStorage.maxChunkCount)
await browser.storage.sync.remove(getChunkKeys(chunks.length));
} }
catch (ex)
const data: string = compress(serializeCollections(collections), { outputEncoding: "Base64" });
const chunks: string[] = splitIntoChunks(data);
if (chunks.length > collectionStorage.maxChunkCount)
throw new Error("Data is too large to be stored in sync storage.");
// Since there's a limit for cloud write operations, we need to write all chunks in one go.
const newRecords: Record<string, string | number> =
{ {
logger("Failed to save cloud storage"); [getStorageKey(collectionStorage.chunkCount)]: chunks.length,
console.error(ex); [getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
trackError("cloud_save_error", ex as Error); };
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE")) for (let i = 0; i < chunks.length; i++)
await sendNotification({ newRecords[`c${i}`] = chunks[i];
title: i18n.t("notifications.error_quota_exceeded.title"),
message: i18n.t("notifications.error_quota_exceeded.message"), await browser.storage.sync.set(newRecords);
icon: "/notification_icons/cloud_error.png"
}); if (chunks.length < collectionStorage.maxChunkCount)
else await browser.storage.sync.remove(getChunkKeys(chunks.length));
await sendNotification({
title: i18n.t("notifications.error_storage_full.title"),
message: i18n.t("notifications.error_storage_full.message"),
icon: "/notification_icons/cloud_error.png"
});
}
} }
function splitIntoChunks(data: string): string[] 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 = (browser.storage.sync.QUOTA_BYTES_PER_ITEM ?? 8192) - chunkKey.length - 2; const chunkSize = (chrome.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,49 +0,0 @@
import { Unwatch, WatchCallback } from "wxt/utils/storage";
const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
{
getValue: async (): Promise<boolean> =>
await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] }),
watch: (cb: WatchCallback<boolean>): Unwatch =>
{
const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
{
if (permissions.permissions?.includes("scripting") || permissions.origins?.includes("<all_urls>"))
{
const isGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] });
console.log("thumbnailCaptureEnabled changed", isGranted);
cb(isGranted, !isGranted);
}
};
browser.permissions.onAdded.addListener(listener);
browser.permissions.onRemoved.addListener(listener);
return (): void =>
{
browser.permissions.onAdded.removeListener(listener);
browser.permissions.onRemoved.removeListener(listener);
};
},
setValue: async (value: boolean): Promise<void> =>
{
let result: boolean = false;
if (value)
result = await browser.permissions.request({ permissions: ["scripting"], origins: ["<all_urls>"] });
else
{
result = await browser.permissions.remove({ origins: ["<all_urls>"] });
if (import.meta.env.DEV)
await browser.permissions.request({ origins: ["http://localhost/*"] });
}
if (!result)
throw new Error("Permission request was denied");
}
};
export default thumbnailCaptureEnabled;
@@ -1,132 +0,0 @@
import { githubLinks } from "@/data/links";
import { analyticsPermission } from "@/features/analytics";
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
import extLink from "@/utils/extLink";
import * as fui from "@fluentui/react-components";
import { Unwatch } from "wxt/utils/storage";
import { reviewSettings } from "../utils/setSettingsReviewNeeded";
import { settingsForReview } from "../utils/showSettingsReviewDialog";
export default function SettingsReviewDialog(): React.ReactElement
{
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
const [captureThumbnails, setCaptureThumbnails] = useState<boolean | null>(null);
const [needsReview, setNeedsReview] = useState<string[]>([]);
const cls = useStyles();
useEffect(() =>
{
analyticsPermission.getValue().then(setAllowAnalytics);
thumbnailCaptureEnabled.getValue().then(setCaptureThumbnails);
settingsForReview.getValue().then(setNeedsReview);
const unwatchAnalytics: Unwatch = analyticsPermission.watch(setAllowAnalytics);
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setCaptureThumbnails);
return () =>
{
unwatchAnalytics();
unwatchThumbnails();
};
}, []);
const updateAnalytics = (enabled: boolean): void =>
{
setAllowAnalytics(null);
analyticsPermission.setValue(enabled)
.catch(() => setAllowAnalytics(!enabled));
};
const updateThumbnails = (enabled: boolean): void =>
{
setCaptureThumbnails(null);
thumbnailCaptureEnabled.setValue(enabled)
.catch(() => setCaptureThumbnails(!enabled));
};
return (
<fui.DialogSurface>
<fui.DialogBody>
<fui.DialogTitle>{ i18n.t("features.settingsReview.title") }</fui.DialogTitle>
<fui.DialogContent className={ cls.content }>
{ needsReview.includes(reviewSettings.THUMBNAILS) &&
<div className={ cls.section }>
<fui.Switch
label={ i18n.t("options_page.storage.thumbnail_capture") }
checked={ captureThumbnails ?? true }
disabled={ captureThumbnails === null }
onChange={ (_, e) => updateThumbnails(e.checked as boolean) } />
<fui.MessageBar layout="multiline">
<fui.MessageBarBody className={ cls.msgBarBody }>
<fui.MessageBarTitle>
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }
</fui.MessageBarTitle>
<fui.Text as="p">
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
</fui.Text>
</fui.MessageBarBody>
</fui.MessageBar>
</div>
}
{ needsReview.includes(reviewSettings.ANALYTICS) &&
<div className={ cls.section }>
<fui.Switch
label={ i18n.t("options_page.general.options.allow_analytics") }
checked={ allowAnalytics ?? true }
disabled={ allowAnalytics === null }
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
<fui.MessageBar layout="multiline">
<fui.MessageBarBody className={ cls.msgBarBody }>
<fui.MessageBarTitle>
{ i18n.t("features.settingsReview.analytics.title") }
</fui.MessageBarTitle>
<fui.Text as="p">
{ i18n.t("features.settingsReview.analytics.p1") }
</fui.Text>
<fui.Text as="p" weight="semibold">
{ i18n.t("features.settingsReview.analytics.p2") }
</fui.Text>
<fui.Text as="p">
{ i18n.t("features.settingsReview.analytics.p3_text") } <fui.Link { ...extLink(githubLinks.privacy) }>{ i18n.t("features.settingsReview.analytics.p3_link") }</fui.Link>.
</fui.Text>
</fui.MessageBarBody>
</fui.MessageBar>
</div>
}
</fui.DialogContent>
<fui.DialogActions>
<fui.Button onClick={ () => browser.runtime.openOptionsPage() }>
{ i18n.t("features.settingsReview.action") }
</fui.Button>
<fui.DialogTrigger>
<fui.Button appearance="primary">{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger>
</fui.DialogActions>
</fui.DialogBody>
</fui.DialogSurface>
);
}
const useStyles = fui.makeStyles({
content:
{
display: "flex",
flexFlow: "column",
gap: fui.tokens.spacingVerticalL
},
section:
{
display: "flex",
flexFlow: "column",
gap: fui.tokens.spacingVerticalXS
},
msgBarBody:
{
display: "flex",
flexFlow: "column",
gap: fui.tokens.spacingVerticalXS,
marginBottom: fui.tokens.spacingVerticalXS
}
});
@@ -1,25 +0,0 @@
import { DialogContextType } from "@/contexts/DialogProvider";
import SettingsReviewDialog from "../components/SettingsReviewDialog";
import { settingsForReview } from "../utils/showSettingsReviewDialog";
export default function useSettingsReviewDialog(dialog: DialogContextType): Promise<void>
{
return new Promise<void>(res =>
{
settingsForReview.getValue().then(needsReview =>
{
if (needsReview.length > 0)
dialog.pushCustom(
<SettingsReviewDialog />,
undefined,
() =>
{
settingsForReview.removeValue();
res();
}
);
else
res();
});
});
}
-1
View File
@@ -1 +0,0 @@
export { default as useSettingsReviewDialog } from "./hooks/useSettingsReviewDialog";
-1
View File
@@ -1 +0,0 @@
export { default as setSettingsReviewNeeded } from "./setSettingsReviewNeeded";
@@ -1,65 +0,0 @@
import { analyticsPermission } from "@/features/analytics";
import { settingsForReview } from "./showSettingsReviewDialog";
export default async function setSettingsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<void>
{
const needsReview: string[] = await settingsForReview.getValue();
if (!needsReview.includes(reviewSettings.ANALYTICS) && await checkAnalyticsReviewNeeded(installReason, previousVersion))
needsReview.push(reviewSettings.ANALYTICS);
if (!needsReview.includes(reviewSettings.THUMBNAILS) && await checkThumbnailsReviewNeeded(installReason, previousVersion))
needsReview.push(reviewSettings.THUMBNAILS);
console.log("Settings needing review:", needsReview);
// Add more settings here as needed
if (needsReview.length > 0)
await settingsForReview.setValue(needsReview);
}
export const reviewSettings =
{
ANALYTICS: "analytics",
THUMBNAILS: "thumbnails"
};
async function checkAnalyticsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<boolean>
{
if (installReason === "install")
return !await analyticsPermission.getValue();
if (installReason === "update")
{
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
const cumulative: number = major * 10000 + minor * 100 + patch;
if (cumulative < 30100) // < 3.1.0
return true;
}
if (import.meta.env.DEV)
return true;
return false;
}
async function checkThumbnailsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<boolean>
{
if (installReason === "install")
return true;
if (installReason === "update")
{
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
const cumulative: number = major * 10000 + minor * 100 + patch;
if (cumulative < 30100) // < 3.1.0
return true;
}
if (import.meta.env.DEV)
return true;
return false;
}
@@ -1,6 +0,0 @@
export const settingsForReview = storage.defineItem<string[]>(
"local:settingsForReview",
{
fallback: []
}
);
+7 -15
View File
@@ -1,25 +1,17 @@
import { DialogContextType } from "@/contexts/DialogProvider"; import { useDialog } from "@/contexts/DialogProvider";
import WelcomeDialog from "../components/WelcomeDialog"; import WelcomeDialog from "../components/WelcomeDialog";
import { showWelcomeDialog } from "../utils/showWelcomeDialog"; import { showWelcomeDialog } from "../utils/showWelcomeDialog";
export default function useWelcomeDialog(dialog: DialogContextType): Promise<void> export default function useWelcomeDialog(): void
{ {
return new Promise<void>(res => const dialog = useDialog();
useEffect(() =>
{ {
showWelcomeDialog.getValue().then(showWelcome => showWelcomeDialog.getValue().then(showWelcome =>
{ {
if (showWelcome || import.meta.env.DEV) if (showWelcome || import.meta.env.DEV)
dialog.pushCustom( dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
<WelcomeDialog />,
undefined,
() =>
{
showWelcomeDialog.removeValue();
res();
}
);
else
res();
}); });
}); }, []);
} }
-18
View File
@@ -1,18 +0,0 @@
import { DialogContextType, useDialog } from "@/contexts/DialogProvider";
export default function useDialogTrain(...dialogs: ((dialog: DialogContextType) => Promise<void>)[]): void
{
const dialog = useDialog();
useEffect(() =>
{
(async () =>
{
for (const item of dialogs)
{
await item(dialog);
await new Promise(res => setTimeout(res, 250));
}
})();
}, []);
}
+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<`${Browser.tabGroups.Color}`, string> = makeStyles({ export const useGroupColors: () => Record<chrome.tabGroups.ColorEnum, 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: browser.storage.sync.QUOTA_BYTES ?? 102400, storageQuota: chrome.storage.sync.QUOTA_BYTES ?? 102400,
usedStorageRatio: bytesInUse / (browser.storage.sync.QUOTA_BYTES ?? 102400) usedStorageRatio: bytesInUse / (chrome.storage.sync.QUOTA_BYTES ?? 102400)
}; };
} }
-25
View File
@@ -36,15 +36,6 @@ features:
text3: "Visit our dev blog to learn more about this update and all of its features!" text3: "Visit our dev blog to learn more about this update and all of its features!"
actions: actions:
visit_blog: "Read dev blog" visit_blog: "Read dev blog"
settingsReview:
title: "Review your settings"
action: "All settings"
analytics:
title: "These statistics will help us improve the extension"
p1: "We only collect usage statistics (number of collections, used features, etc.)"
p2: "We do not collect any of your data!"
p3_text: "See the full list of what we collect"
p3_link: "here"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Ask for confirmation when deleting an item" show_delete_prompt: "Ask for confirmation when deleting an item"
show_badge: "Show counter badge" show_badge: "Show counter badge"
show_notification: "Show notification when saving tabs using context menu" show_notification: "Show notification when saving tabs using context menu"
show_partial_save_notification: "Show notification when some tabs couldn't be saved"
unload_tabs: "Do not load tabs after opening" unload_tabs: "Do not load tabs after opening"
allow_analytics: "Allow collection of anonymous statistics"
list_locations: list_locations:
title: "Open collection list in:" title: "Open collection list in:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized." text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized."
action: "Disable and reload the extension" action: "Disable and reload the extension"
thumbnail_capture: "Capture thumbnails and icons for saved tabs"
thumbnail_capture_notice1: "Requires permission to access content on visited websites"
thumbnail_capture_notice2: "Disabling this feature may improve performance on large collections"
clear_thumbnails:
action: "Clear saved thumbnails"
title: "Delete all saved thumbnails?"
prompt: "This action will remove all saved thumbnails, previews and icons for your saved tabs. This action cannot be undone."
about: about:
title: "About" title: "About"
developed_by: "Developed by Eugene Fox" developed_by: "Developed by Eugene Fox"
@@ -151,7 +133,6 @@ options_page:
website: "My website" website: "My website"
source: "Source code" source: "Source code"
changelog: "Changelog" changelog: "Changelog"
privacy: "Privacy policy"
collections: collections:
empty: "This collection is empty" empty: "This collection is empty"
@@ -184,8 +165,6 @@ 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"
@@ -221,25 +200,21 @@ 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:
-25
View File
@@ -36,15 +36,6 @@ features:
text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!" text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!"
actions: actions:
visit_blog: "Leer el blog de desarrollo" visit_blog: "Leer el blog de desarrollo"
settingsReview:
title: "Revisa tus ajustes"
action: "Todos los ajustes"
analytics:
title: "Estas estadísticas nos ayudarán a mejorar la extensión"
p1: "Solo recopilamos estadísticas de uso (número de colecciones, funciones utilizadas, etc.)"
p2: "¡No recopilamos ninguno de tus datos!"
p3_text: "Ver la lista completa de lo que recopilamos"
p3_link: "aquí"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Pedir confirmación al eliminar un elemento" show_delete_prompt: "Pedir confirmación al eliminar un elemento"
show_badge: "Mostrar insignia de contador" show_badge: "Mostrar insignia de contador"
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual" show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
show_partial_save_notification: "Mostrar notificación cuando algunas pestañas no se pudieron guardar"
unload_tabs: "No cargar pestañas después de abrir" unload_tabs: "No cargar pestañas después de abrir"
allow_analytics: "Permitir la recopilación de estadísticas anónimas"
list_locations: list_locations:
title: "Abrir lista de colecciones en:" title: "Abrir lista de colecciones en:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "Esta acción deshabilitará la sincronización de colecciones entre tus dispositivos. La configuración de la extensión seguirá sincronizándose." text: "Esta acción deshabilitará la sincronización de colecciones entre tus dispositivos. La configuración de la extensión seguirá sincronizándose."
action: "Deshabilitar y recargar la extensión" action: "Deshabilitar y recargar la extensión"
thumbnail_capture: "Capturar miniaturas e íconos para las pestañas guardadas"
thumbnail_capture_notice1: "Requiere permiso para acceder al contenido de los sitios web visitados"
thumbnail_capture_notice2: "Deshabilitar esta función puede mejorar el rendimiento en colecciones grandes"
clear_thumbnails:
action: "Eliminar miniaturas guardadas"
title: "¿Eliminar todas las miniaturas guardadas?"
prompt: "Esta acción eliminará todas las miniaturas, vistas previas e íconos guardados para tus pestañas guardadas. Esta acción no se puede deshacer."
about: about:
title: "Acerca de" title: "Acerca de"
developed_by: "Desarrollado por Eugene Fox" developed_by: "Desarrollado por Eugene Fox"
@@ -151,7 +133,6 @@ options_page:
website: "Mi sitio web" website: "Mi sitio web"
source: "Código fuente" source: "Código fuente"
changelog: "Registro de cambios" changelog: "Registro de cambios"
privacy: "Política de privacidad"
collections: collections:
empty: "Esta colección está vacía" empty: "Esta colección está vacía"
@@ -184,8 +165,6 @@ 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"
@@ -221,25 +200,21 @@ 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:
-25
View File
@@ -36,15 +36,6 @@ features:
text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!" text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!"
actions: actions:
visit_blog: "Leggi il blog degli sviluppatori" visit_blog: "Leggi il blog degli sviluppatori"
settingsReview:
title: "Rivedi le tue impostazioni"
action: "Tutte le impostazioni"
analytics:
title: "Queste statistiche ci aiuteranno a migliorare l'estensione"
p1: "Raccogliamo solo statistiche di utilizzo (numero di collezioni, funzionalità utilizzate, ecc.)"
p2: "Non raccogliamo nessuno dei tuoi dati!"
p3_text: "Vedi l'elenco completo di ciò che raccogliamo"
p3_link: "qui"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Chiedi conferma quando elimini un elemento" show_delete_prompt: "Chiedi conferma quando elimini un elemento"
show_badge: "Mostra il badge del contatore" show_badge: "Mostra il badge del contatore"
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale" show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
show_partial_save_notification: "Mostra notifica quando alcune schede non sono state salvate"
unload_tabs: "Non caricare le schede dopo l'apertura" unload_tabs: "Non caricare le schede dopo l'apertura"
allow_analytics: "Consenti la raccolta di statistiche anonime"
list_locations: list_locations:
title: "Apri elenco delle collezioni in:" title: "Apri elenco delle collezioni in:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "Questa azione disabiliterà la sincronizzazione delle collezioni tra i tuoi dispositivi. Le impostazioni dell'estensione saranno comunque sincronizzate." text: "Questa azione disabiliterà la sincronizzazione delle collezioni tra i tuoi dispositivi. Le impostazioni dell'estensione saranno comunque sincronizzate."
action: "Disabilita e ricarica l'estensione" action: "Disabilita e ricarica l'estensione"
thumbnail_capture: "Cattura miniature e icone per le schede salvate"
thumbnail_capture_notice1: "Richiede il permesso di accedere ai contenuti dei siti web visitati"
thumbnail_capture_notice2: "Disabilitare questa funzione può migliorare le prestazioni su collezioni di grandi dimensioni"
clear_thumbnails:
action: "Elimina miniature salvate"
title: "Eliminare tutte le miniature salvate?"
prompt: "Questa azione rimuoverà tutte le miniature, anteprime e icone salvate per le tue schede salvate. Questa azione non può essere annullata."
about: about:
title: "Informazioni" title: "Informazioni"
developed_by: "Sviluppato da Eugene Fox" developed_by: "Sviluppato da Eugene Fox"
@@ -151,7 +133,6 @@ options_page:
website: "Il mio sito web" website: "Il mio sito web"
source: "Codice sorgente" source: "Codice sorgente"
changelog: "Registro delle modifiche" changelog: "Registro delle modifiche"
privacy: "Politica sulla riservatezza"
collections: collections:
empty: "Questa collezione è vuota" empty: "Questa collezione è vuota"
@@ -184,8 +165,6 @@ 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"
@@ -221,25 +200,21 @@ 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:
-25
View File
@@ -36,15 +36,6 @@ features:
text3: "Odwiedź blog dewelopera (tylko w języku angielskim), aby dowiedzieć się więcej o tej aktualizacji i wszystkich jej funkcjach!" text3: "Odwiedź blog dewelopera (tylko w języku angielskim), aby dowiedzieć się więcej o tej aktualizacji i wszystkich jej funkcjach!"
actions: actions:
visit_blog: "Czytaj blog" visit_blog: "Czytaj blog"
settingsReview:
title: "Sprawdź ustawienia"
action: "Wszystkie ustawienia"
analytics:
title: "Ta statystyka pozwoli ulepszać rozszerzenie"
p1: "Zbieramy tylko statystyki użycia (liczba kolekcji, używane funkcje itp.)"
p2: "Nie zbieramy twoich danych osobowych!"
p3_text: "Pełną listę zbieranych danych można zobaczyć"
p3_link: "tutaj"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Pytaj o potwierdzenie przy usuwaniu elementów" show_delete_prompt: "Pytaj o potwierdzenie przy usuwaniu elementów"
show_badge: "Pokaż licznik" show_badge: "Pokaż licznik"
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe" show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
show_partial_save_notification: "Pokaż powiadomienie, jeśli niektóre karty nie zostały zapisane"
unload_tabs: "Nie ładuj kart po otwarciu" unload_tabs: "Nie ładuj kart po otwarciu"
allow_analytics: "Zezwól na zbieranie anonimowej statystyki"
list_locations: list_locations:
title: "Otwieraj listę kolekcji w:" title: "Otwieraj listę kolekcji w:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "Ta akcja wyłączy synchronizację kolekcji między twoimi urządzeniami. Ustawienia nadal będą przechowywane w chmurze." text: "Ta akcja wyłączy synchronizację kolekcji między twoimi urządzeniami. Ustawienia nadal będą przechowywane w chmurze."
action: "Wyłącz i przeładuj rozszerzenie" action: "Wyłącz i przeładuj rozszerzenie"
thumbnail_capture: "Zapisuj podglądy i ikony dla zapisanych kart"
thumbnail_capture_notice1: "Wymagany dostęp do zawartości odwiedzanych stron internetowych"
thumbnail_capture_notice2: "Wyłączenie tej funkcji może poprawić wydajność przy dużej liczbie zapisanych kart"
clear_thumbnails:
action: "Usuń zapisane ikony"
title: "Usunąć podglądy i ikony?"
prompt: "Ta akcja usunie wszystkie podglądy i ikony twoich zapisanych kart. Tej akcji nie można cofnąć."
about: about:
title: "O rozszerzeniu" title: "O rozszerzeniu"
developed_by: "Wywoływacz: Eugeniusz Lis" developed_by: "Wywoływacz: Eugeniusz Lis"
@@ -151,7 +133,6 @@ options_page:
website: "Moja strona internetowa" website: "Moja strona internetowa"
source: "Kod źródłowy" source: "Kod źródłowy"
changelog: "Lista zmian" changelog: "Lista zmian"
privacy: "Polityka prywatności"
collections: collections:
empty: "Ta kolekcja jest pusta" empty: "Ta kolekcja jest pusta"
@@ -184,8 +165,6 @@ 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"
@@ -221,25 +200,21 @@ 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:
-25
View File
@@ -36,15 +36,6 @@ features:
text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!" text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!"
actions: actions:
visit_blog: "Ler blog de desenvolvimento" visit_blog: "Ler blog de desenvolvimento"
settingsReview:
title: "Revise suas configurações"
action: "Todas as configurações"
analytics:
title: "Estas estatísticas nos ajudarão a melhorar a extensão"
p1: "Nós coletamos apenas estatísticas de uso (número de coleções, recursos usados, etc.)"
p2: "Nós não coletamos nenhum dos seus dados!"
p3_text: "Veja a lista completa do que coletamos"
p3_link: "aqui"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Pedir confirmação ao excluir um item" show_delete_prompt: "Pedir confirmação ao excluir um item"
show_badge: "Mostrar contador no ícone" show_badge: "Mostrar contador no ícone"
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto" show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
show_partial_save_notification: "Mostrar notificação quando algumas abas não puderam ser salvas"
unload_tabs: "Não carregar abas após abrir" unload_tabs: "Não carregar abas após abrir"
allow_analytics: "Permitir coleta de estatísticas anônimas"
list_locations: list_locations:
title: "Abrir lista de coleções em:" title: "Abrir lista de coleções em:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "Esta ação desativará a sincronização de coleções entre seus dispositivos. As configurações da extensão ainda serão sincronizadas." text: "Esta ação desativará a sincronização de coleções entre seus dispositivos. As configurações da extensão ainda serão sincronizadas."
action: "Desativar e recarregar a extensão" action: "Desativar e recarregar a extensão"
thumbnail_capture: "Capturar miniaturas e ícones para as abas salvas"
thumbnail_capture_notice1: "Requer permissão para acessar o conteúdo dos sites visitados"
thumbnail_capture_notice2: "Desativar esse recurso pode melhorar o desempenho em coleções grandes"
clear_thumbnails:
action: "Eliminar miniaturas guardadas"
title: "Excluir todas as miniaturas salvas?"
prompt: "Esta ação removerá todas as miniaturas, pré-visualizações e ícones salvos para suas abas salvas. Esta ação não pode ser desfeita."
about: about:
title: "Sobre" title: "Sobre"
developed_by: "Desenvolvido por Eugene Fox" developed_by: "Desenvolvido por Eugene Fox"
@@ -151,7 +133,6 @@ options_page:
website: "Meu site" website: "Meu site"
source: "Código-fonte" source: "Código-fonte"
changelog: "Registro de alterações" changelog: "Registro de alterações"
privacy: "Política de Privacidade"
collections: collections:
empty: "Esta coleção está vazia" empty: "Esta coleção está vazia"
@@ -184,8 +165,6 @@ 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"
@@ -221,25 +200,21 @@ 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:
+4 -29
View File
@@ -36,15 +36,6 @@ features:
text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!" text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!"
actions: actions:
visit_blog: "Читать блог" visit_blog: "Читать блог"
settingsReview:
title: "Проверьте настройки"
action: "Все настройки"
analytics:
title: "Эта статистика позволит улучшать расширение"
p1: "Мы собираем только статистику использования (количество коллекций, используемые функции и т.д.)"
p2: "Мы не собираем ваши личные данные!"
p3_text: "Полный список собираемых данных можно посмотреть"
p3_link: "здесь"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Спрашивать подтверждение при удалении элементов" show_delete_prompt: "Спрашивать подтверждение при удалении элементов"
show_badge: "Показывать счетчик" show_badge: "Показывать счетчик"
show_notification: "Показывать уведомление при сохранении через контекстное меню" show_notification: "Показывать уведомление при сохранении через контекстное меню"
show_partial_save_notification: "Показывать уведомление, если некоторые вкладки не были сохранены"
unload_tabs: "Не загружать вкладки после открытия" unload_tabs: "Не загружать вкладки после открытия"
allow_analytics: "Разрешить сбор анонимной статистики"
list_locations: list_locations:
title: "Открывать список коллекций в:" title: "Открывать список коллекций в:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке." text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке."
action: "Отключить и перезагрузить расширение" action: "Отключить и перезагрузить расширение"
thumbnail_capture: "Сохранять превью и иконки для сохранённых вкладок"
thumbnail_capture_notice1: "Необходим доступ к содержанию посещенных веб-сайтов"
thumbnail_capture_notice2: "Отключение этой функции может улучшить производительность при большом количестве сохраненных вкладок"
clear_thumbnails:
action: "Удалить сохранённые иконки"
title: "Удалить превью и иконки?"
prompt: "Это действие удалит все превью и иконки у ваших сохраненных вкладок. Это действие не может быть отменено."
about: about:
title: "О расширении" title: "О расширении"
developed_by: "Разработчик: Евгений Лис" developed_by: "Разработчик: Евгений Лис"
@@ -151,7 +133,6 @@ options_page:
website: "Мой веб-сайт" website: "Мой веб-сайт"
source: "Исходный код" source: "Исходный код"
changelog: "Список изменений" changelog: "Список изменений"
privacy: "Политика конфиденциальности"
collections: collections:
empty: "Эта коллекция пуста" empty: "Эта коллекция пуста"
@@ -168,13 +149,13 @@ collections:
title: "Требуется разрешение" title: "Требуется разрешение"
message: message:
edge: edge:
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме InPrivate" p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме InPrivate"
p2: "Для этого нажмите \"Настройки\" и затем отметьте опцию \"Разрешить в режиме InPrivate\"" p2: "Для этого нажмите \"Настройки\" и затем отметьте опцию \"Разрешить в режиме InPrivate\""
firefox: firefox:
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в приватном окне" p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в приватном окне"
p2: "Для этого нажмите \"Настройки\", перейдите в \"Подробности\" и разрешите \"Запуск в приватных окнах\"" p2: "Для этого нажмите \"Настройки\", перейдите в \"Подробности\" и разрешите \"Запуск в приватных окнах\""
chrome: chrome:
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме инкогнито" p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме инкогнито"
p2: "Для этого нажмите \"Настройки\" и отметьте опцию \"Разрешить использование в режиме инкогнито\"" p2: "Для этого нажмите \"Настройки\" и отметьте опцию \"Разрешить использование в режиме инкогнито\""
action: "Настройки" action: "Настройки"
menu: menu:
@@ -184,8 +165,6 @@ collections:
add_group: "Добавить пустую группу" add_group: "Добавить пустую группу"
export_bookmarks: "Экспортировать в закладки" export_bookmarks: "Экспортировать в закладки"
edit: "Редактировать коллекцию" edit: "Редактировать коллекцию"
hide: "Скрыть коллекцию"
unhide: "Показать коллекцию"
groups: groups:
title: "Группа" title: "Группа"
@@ -221,25 +200,21 @@ 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:
@@ -271,6 +246,6 @@ parse_error_message:
merge_conflict_message: merge_conflict_message:
title: "В локальном и облачном хранилищах есть конфликтующие изменения." title: "В локальном и облачном хранилищах есть конфликтующие изменения."
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако либо принять изменения из облака." message: "Чтобы это исправить, вы можете сохранить локальную копию в облако, либо принять изменения из облака."
accept_local: "Заменить локальной" accept_local: "Заменить локальной"
accept_cloud: "Принять облачные изменения" accept_cloud: "Принять облачные изменения"
+1 -26
View File
@@ -36,15 +36,6 @@ features:
text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!" text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!"
actions: actions:
visit_blog: "Читати блог" visit_blog: "Читати блог"
settingsReview:
title: "Перевірте налаштування"
action: "Всi налаштування"
analytics:
title: "Ця статистика дозволить покращувати розширення"
p1: "Ми збираємо лише статистику використання (кількість колекцій, використовувані функції тощо)"
p2: "Ми не збираємо ваші особисті дані!"
p3_text: "Повний список зібраних даних можна подивитися"
p3_link: "тут"
notifications: notifications:
tabs_saved: tabs_saved:
@@ -82,9 +73,7 @@ options_page:
show_delete_prompt: "Запитувати підтвердження при видаленні елементів" show_delete_prompt: "Запитувати підтвердження при видаленні елементів"
show_badge: "Показувати лічильник" show_badge: "Показувати лічильник"
show_notification: "Показувати сповіщення при збереженні через контекстне меню" show_notification: "Показувати сповіщення при збереженні через контекстне меню"
show_partial_save_notification: "Показувати сповіщення, якщо деякі вкладки не були збережені"
unload_tabs: "Не завантажувати вкладки після відкриття" unload_tabs: "Не завантажувати вкладки після відкриття"
allow_analytics: "Дозволити збір анонімної статистики"
list_locations: list_locations:
title: "Відкривати список колекцій у:" title: "Відкривати список колекцій у:"
options: options:
@@ -132,13 +121,6 @@ options_page:
disable_prompt: disable_prompt:
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі." text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
action: "Вимкнути та перезавантажити розширення" action: "Вимкнути та перезавантажити розширення"
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок"
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
clear_thumbnails:
action: "Видалити збережені іконки"
title: "Видалити превью і іконки?"
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
about: about:
title: "О розширенні" title: "О розширенні"
developed_by: "Розробник: Євген Лис" developed_by: "Розробник: Євген Лис"
@@ -151,7 +133,6 @@ options_page:
website: "Мій веб-сайт" website: "Мій веб-сайт"
source: "Вихідний код" source: "Вихідний код"
changelog: "Список змін" changelog: "Список змін"
privacy: "Політика конфіденційності"
collections: collections:
empty: "Ця колекція пуста" empty: "Ця колекція пуста"
@@ -184,8 +165,6 @@ collections:
add_group: "Додати порожню групу" add_group: "Додати порожню групу"
export_bookmarks: "Експортувати в закладки" export_bookmarks: "Експортувати в закладки"
edit: "Редагувати колекцію" edit: "Редагувати колекцію"
hide: "Приховати колекцію"
unhide: "Показати колекцію"
groups: groups:
title: "Група" title: "Група"
@@ -221,25 +200,21 @@ 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:
@@ -271,6 +246,6 @@ parse_error_message:
merge_conflict_message: merge_conflict_message:
title: "В локальному і облачному хранилищах є конфліктуючі зміни." title: "В локальному і облачному хранилищах є конфліктуючі зміни."
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі або прийняти зміни з хмари." message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі, або прийняти зміни з хмари."
accept_local: "Заменить локальною" accept_local: "Заменить локальною"
accept_cloud: "Прийняти облачні зміни" accept_cloud: "Прийняти облачні зміни"
+48 -73
View File
@@ -5,8 +5,8 @@ manifest:
shortcuts: shortcuts:
toggle_sidebar: "打开收藏列表" toggle_sidebar: "打开收藏列表"
set_aside: "搁置标签页" set_aside: "将标签放到一边"
save_tabs: "保存标签页但不关闭" save_tabs: "保存标签不关闭"
common: common:
actions: actions:
@@ -14,10 +14,10 @@ common:
save: "保存" save: "保存"
close: "关闭" close: "关闭"
delete: "删除" delete: "删除"
reset_filters: "重置筛选" reset_filters: "重置筛选"
cta: cta:
feedback: "留下反馈" feedback: "留下反馈"
sponsor: "请我喝咖啡" sponsor: "请我喝咖啡"
tooltips: tooltips:
more: "更多" more: "更多"
delete_prompt: "您确定吗?此操作无法撤销。" delete_prompt: "您确定吗?此操作无法撤销。"
@@ -25,51 +25,42 @@ common:
features: features:
v3welcome: v3welcome:
title: "欢迎使用搁置的标签页 3.0" title: "欢迎使用搁置的标签页 3.0"
text1: "我们很高兴宣布搁置的标签页扩展的重大更新!" text1: "我们很高兴宣布搁置的标签页扩展的重大更新!"
text2: "此更新带来了全新的用户界面,以及许多新功能,包括:" text2: "此更新带来了全新的用户界面,以及许多新功能,包括:"
list: list:
item1: "支持标签组" item1: "支持标签组"
item2: "收藏自定义" item2: "收藏自定义"
item3: "拖放排序和整理" item3: "拖放重新排序和组织"
item4: "从开始创建收藏" item4: "从开始手动创建收藏"
item5: "以及更多!" item5: "以及更多!"
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!" text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
actions: actions:
visit_blog: "阅读开发博客" visit_blog: "阅读开发博客"
settingsReview:
title: "检查您的设置"
action: "所有设置"
analytics:
title: "这些统计数据将帮助我们改进扩展"
p1: "我们只收集使用统计数据(收藏数量、使用的功能等)"
p2: "我们不会收集您的任何数据!"
p3_text: "请参阅我们收集内容的"
p3_link: "完整列表"
notifications: notifications:
tabs_saved: tabs_saved:
title: "已创建新收藏" title: "新收藏已创建"
message: "您的标签已保存到新收藏中" message: "您的标签已保存到新收藏中"
error_quota_exceeded: error_quota_exceeded:
title: "超出最大云储存写入操作" title: "超出最大云写入操作"
message: "我们已将您的标签保存到本地存储。您需要手动更新云存储" message: "我们已将您的标签保存到本地存储。您需要手动更新云存储"
error_storage_full: error_storage_full:
title: "您的云存储已满" title: "您的云存储已满"
message: "我们已将您的标签保存到本地存储。请清理一些云存储空间" message: "我们已将您的标签保存到本地存储。请清理一些云存储空间"
bookmark_saved: bookmark_saved:
title: "已导出到书签" title: "已导出到书签"
message: "您的收藏已导出到书签" message: "您的收藏已导出到书签"
partial_save: partial_save:
title: "部分标签无法保存" title: "某些标签无法保存"
message: "部分标签页是无法访问的系统标签。它们已被跳过" message: "某些标签是我们无法访问的系统标签。它们已被跳过"
actions: actions:
save: save:
all: "保存所有标签" all: "保存所有标签"
selected: "保存选定的标签" selected: "保存选定的标签"
set_aside: set_aside:
all: "搁置所有标签" all: "所有标签放到一边"
selected: "搁置选定的标签" selected: "选定的标签放到一边"
show_collections: "显示收藏" show_collections: "显示收藏"
options_page: options_page:
@@ -78,13 +69,11 @@ options_page:
title: "常规" title: "常规"
options: options:
always_show_toolbars: "始终显示工具栏" always_show_toolbars: "始终显示工具栏"
include_pinned: "保存所有标签时包括固定标签" include_pinned: "保存所有标签时包括固定标签"
show_delete_prompt: "删除项目时要求确认" show_delete_prompt: "删除项目时要求确认"
show_badge: "显示计数角标" show_badge: "显示计数徽章"
show_notification: "使用上下文菜单保存标签时显示通知" show_notification: "使用上下文菜单保存标签时显示通知"
show_partial_save_notification: "如果某些标签页无法保存则显示通知" unload_tabs: "打开后不加载标签"
unload_tabs: "打开后不加载标签页"
allow_analytics: "允许收集匿名统计数据"
list_locations: list_locations:
title: "在以下位置打开收藏列表:" title: "在以下位置打开收藏列表:"
options: options:
@@ -103,15 +92,15 @@ options_page:
title: "默认操作" title: "默认操作"
options: options:
save_actions: save_actions:
title: "保存标签时的默认操作" title: "保存标签时的默认操作"
options: options:
set_aside: "保存并关闭标签" set_aside: "保存并关闭标签"
save: "保存标签而不关闭" save: "保存标签而不关闭"
restore_actions: restore_actions:
title: "打开收藏时的默认操作" title: "打开收藏时的默认操作"
options: options:
open: "仅打开标签" open: "仅打开标签"
restore: "打开标签并删除收藏" restore: "打开标签并删除收藏"
storage: storage:
title: "存储" title: "存储"
capacity: capacity:
@@ -125,37 +114,29 @@ options_page:
import_prompt: import_prompt:
title: "导入数据" title: "导入数据"
warning_title: "这是不可逆的操作" warning_title: "这是不可逆的操作"
warning_text: "这将覆盖您的所有数据请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。" warning_text: "这将覆盖您的所有数据请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
proceed: "选择文件" proceed: "选择文件"
enable: "启用云存储" enable: "启用云存储"
disable: "禁用云存储" disable: "禁用云存储"
disable_prompt: disable_prompt:
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。" text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
action: "禁用并重新加载扩展" action: "禁用并重新加载扩展"
thumbnail_capture: "为已保存的标签页保存缩略图和图标"
thumbnail_capture_notice1: "需要访问已访问网站内容的权限"
thumbnail_capture_notice2: "有大量收藏时,禁用此功能可能会提高性能"
clear_thumbnails:
action: "删除已保存的图标"
title: "删除缩略图和图标?"
prompt: "此操作将删除您已保存标签页的所有缩略图和图标。此操作无法撤消。"
about: about:
title: "关于" title: "关于"
developed_by: "由尤金·福克斯开发" developed_by: "由尤金·福克斯开发"
licensed_under: "许可协议" licensed_under: "许可协议"
mit_license: "MIT 协议" mit_license: "MIT 许可协议"
translation_cta: translation_cta:
text: "发现错别字或想为您的语言提供翻译?" text: "发现错别字或想为您的语言提供翻译?"
button: "快速入门" button: "从这里开始"
links: links:
website: "我的网站" website: "我的网站"
source: "源代码" source: "源代码"
changelog: "更新日志" changelog: "更新日志"
privacy: "隐私政策"
collections: collections:
empty: "此收藏为空" empty: "此收藏为空"
tabs_count: "$1 个标签" tabs_count: "$1 个标签"
actions: actions:
open: "打开所有" open: "打开所有"
restore: "恢复所有" restore: "恢复所有"
@@ -171,37 +152,35 @@ collections:
p1: "扩展需要权限才能在 InPrivate 窗口中打开标签" p1: "扩展需要权限才能在 InPrivate 窗口中打开标签"
p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项" p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项"
firefox: firefox:
p1: "扩展需要权限才能在隐私窗口中打开标签" p1: "扩展需要权限才能在隐私窗口中打开标签"
p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”" p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”"
chrome: chrome:
p1: "扩展需要权限才能在隐身窗口中打开标签" p1: "扩展需要权限才能在隐身窗口中打开标签"
p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项" p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项"
action: "设置" action: "设置"
menu: menu:
delete: "删除收藏" delete: "删除收藏"
add_selected: "添加选定的标签" add_selected: "添加选定的标签"
add_all: "添加所有标签" add_all: "添加所有标签"
add_group: "添加空组" add_group: "添加空组"
export_bookmarks: "导出到书签" export_bookmarks: "导出到书签"
edit: "编辑收藏" edit: "编辑收藏"
hide: "隐藏收藏"
unhide: "显示收藏"
groups: groups:
title: "组" title: "组"
pinned: "已固定" pinned: "已固定"
open: "打开所有" open: "打开所有"
empty: "此组为空" empty: "此组为空"
menu: menu:
new_window: "在新窗口中打开" new_window: "在新窗口中打开"
add_selected: "添加选定的标签" add_selected: "添加选定的标签"
add_all: "添加所有标签" add_all: "添加所有标签"
edit: "编辑组" edit: "编辑组"
ungroup: "取消分组" ungroup: "取消分组"
delete: "删除组" delete: "删除组"
tabs: tabs:
delete: "删除标签" delete: "删除标签"
colors: colors:
none: "无颜色" none: "无颜色"
@@ -220,26 +199,22 @@ dialogs:
edit: edit:
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:
@@ -250,7 +225,7 @@ main:
custom: "自定义" custom: "自定义"
empty: empty:
title: "这里还没有内容" title: "这里还没有内容"
message: "搁置当前标签,或创建新收藏" message: "当前标签放到一边,或创建新收藏"
empty_search: empty_search:
title: "未找到任何内容" title: "未找到任何内容"
message: "尝试更改搜索查询" message: "尝试更改搜索查询"
@@ -272,5 +247,5 @@ parse_error_message:
merge_conflict_message: merge_conflict_message:
title: "您的本地和云存储有冲突的更改。" title: "您的本地和云存储有冲突的更改。"
message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。" message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。"
accept_local: "用本地替换云端" accept_local: "用本地替换"
accept_cloud: "接受云端更改" accept_cloud: "接受云端更改"
+2 -3
View File
@@ -17,7 +17,7 @@ export type DefaultGroupItem =
type: "group"; type: "group";
pinned?: false; pinned?: false;
title?: string; title?: string;
color: `${Browser.tabGroups.Color}`; color: chrome.tabGroups.ColorEnum;
items: TabItem[]; items: TabItem[];
}; };
@@ -28,9 +28,8 @@ export type CollectionItem =
type: "collection"; type: "collection";
timestamp: number; timestamp: number;
title?: string; title?: string;
color?: `${Browser.tabGroups.Color}`; color?: chrome.tabGroups.ColorEnum;
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
+28 -23
View File
@@ -1,14 +1,15 @@
{ {
"name": "tabs-aside", "name": "tabs-aside",
"private": true, "private": true,
"version": "3.3.1", "version": "3.0.0-rc7",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "wxt", "dev": "wxt",
"build": "npm run lint && wxt build --mv3", "build": "wxt build --mv3",
"zip": "npm run lint && wxt zip --mv3", "zip": "wxt zip --mv3",
"lint": "tsc --noEmit && eslint . -c eslint.config.js", "lint": "tsc --noEmit && eslint . -c eslint.config.js",
"prepare": "wxt prepare", "prebuild": "yarn lint",
"prezip": "yarn lint",
"postinstall": "wxt prepare" "postinstall": "wxt prepare"
}, },
"dependencies": { "dependencies": {
@@ -16,27 +17,31 @@
"@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.73.8", "@fluentui/react-components": "^9.63.0",
"@fluentui/react-icons": "^2.0.326", "@fluentui/react-icons": "^2.0.298",
"@webext-core/messaging": "^2.3.0", "@webext-core/messaging": "^2.2.0",
"@wxt-dev/analytics": "^0.5.4", "@wxt-dev/analytics": "^0.4.1",
"@wxt-dev/i18n": "^0.2.5", "@wxt-dev/i18n": "^0.2.3",
"lzutf8": "^0.6.3", "lzutf8": "^0.6.3",
"react": "^19.2.6", "react": "^18.3.1",
"react-dom": "^19.2.6" "react-dom": "18.3.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/css": "^0.14.1", "@eslint/css": "^0.7.0",
"@eslint/js": "^9.39.4", "@eslint/js": "^9.26.0",
"@eslint/json": "^0.14.0", "@eslint/json": "^0.12.0",
"@stylistic/eslint-plugin": "^5.10.0", "@stylistic/eslint-plugin": "^4.2.0",
"@types/react": "^19.2.14", "@types/react": "^18.3.1",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^18.3.1",
"@wxt-dev/module-react": "^1.2.2", "@wxt-dev/module-react": "^1.1.3",
"eslint": "^9.39.4", "eslint": "^9.26.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"typescript": "^6.0.3", "globals": "^16.0.0",
"typescript-eslint": "^8.59.3", "scheduler": "0.23.0",
"wxt": "^0.20.26" "typescript": "^5.8.3",
} "typescript-eslint": "^8.31.1",
"vite": "^6.3.4",
"wxt": "~0.19.29"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }
-85
View File
@@ -1,85 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.6 KiB

+13 -11
View File
@@ -31,17 +31,19 @@ Stemming its roots from the original Microsoft Edge browser feature, this extens
It allows you to save and manage your tabs in a convenient way, providing a range of features that make it easy to organize and access your saved tabs. It allows you to save and manage your tabs in a convenient way, providing a range of features that make it easy to organize and access your saved tabs.
**Features** <b>Features</b>
- **Save tabs:** Save all your open tabs in a single click, and restore them later <ul>
- **Organize tabs:** Create collections and subgroups to organize your saved tabs <li><b>Save tabs</b>: Save all your open tabs in a single click, and restore them later</li>
- **Search tabs:** Quickly find the tabs you need using the search feature <li><b>Organize tabs</b>: Create collections and subgroups to organize your saved tabs</li>
- **Sync across devices:** Access your saved tabs from any device with your account <li><b>Search tabs</b>: Quickly find the tabs you need using the search feature</li>
- **Go dark:** Dark mode support for a more comfortable browsing experience <li><b>Sync across devices</b>: Access your saved tabs from any device with your account</li>
- **Personalize:** Change the appearance and behavior of the extension to suit your needs <li><b>Go dark</b>: Dark mode support for a more comfortable browsing experience</li>
<li><b>Personalize</b>: Change the appearance and behavior of the extension to suit your needs</li>
</ul>
Check out our [blog post](https://at.xfox111.net/tabs-aside-3-0) regarding all the new features and improvements in Tabs aside 3.0 Check out our <a href="https://at.xfox111.net/tabs-aside-3-0">blog post</a> regarding all the new features and improvements in Tabs aside 3.0
**Hey, it's an open-source software!** <b>Hey, it's an open-source software!</b>
If you know how to improve this extension you can check [its GitHub Repository](https://github.com/xfox111/TabsAsideExtension) If you know how to improve this extension you can check <a href="https://github.com/xfox111/TabsAsideExtension">its GitHub Repository</a>
Check out [release changelog](https://github.com/xfox111/TabsAsideExtension/releases/latest) Check out <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">release changelog</a>
+13 -11
View File
@@ -30,17 +30,19 @@ Basada en la funcionalidad original del navegador Microsoft Edge, esta extensió
Te permite guardar y gestionar tus pestañas de manera conveniente, proporcionando una gama de características que facilitan organizar y acceder a tus pestañas guardadas. Te permite guardar y gestionar tus pestañas de manera conveniente, proporcionando una gama de características que facilitan organizar y acceder a tus pestañas guardadas.
**Características** <b>Características</b>
- **Guardar pestañas:** Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde <ul>
- **Organizar pestañas:** Crea colecciones y subgrupos para organizar tus pestañas guardadas <li><b>Guardar pestañas</b>: Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde</li>
- **Buscar pestañas:** Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda <li><b>Organizar pestañas</b>: Crea colecciones y subgrupos para organizar tus pestañas guardadas</li>
- **Sincronizar entre dispositivos:** Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta <li><b>Buscar pestañas</b>: Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda</li>
- **Modo oscuro:** Soporte para modo oscuro para una experiencia de navegación más cómoda <li><b>Sincronizar entre dispositivos</b>: Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta</li>
- **Personalizar:** Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades <li><b>Modo oscuro</b>: Soporte para modo oscuro para una experiencia de navegación más cómoda</li>
<li><b>Personalizar</b>: Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades</li>
</ul>
Consulta nuestra [publicación en el blog](https://at.xfox111.net/tabs-aside-3-0) sobre todas las nuevas características y mejoras en Pestañas a un lado 3.0 Consulta nuestra <a href="https://at.xfox111.net/tabs-aside-3-0">publicación en el blog</a> sobre todas las nuevas características y mejoras en Pestañas a un lado 3.0
**¡Oye, es un software de código abierto!** <b>¡Oye, es un software de código abierto!</b>
Si sabes cómo mejorar esta extensión, puedes revisar [su repositorio de GitHub](https://github.com/xfox111/TabsAsideExtension) Si sabes cómo mejorar esta extensión, puedes revisar <a href="https://github.com/xfox111/TabsAsideExtension">su repositorio de GitHub</a>
Consulta el [registro de cambios](https://github.com/xfox111/TabsAsideExtension/releases/latest) Consulta el <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">registro de cambios</a>
+13 -11
View File
@@ -30,17 +30,19 @@ Radicata nella funzionalità originale del browser Microsoft Edge, questa estens
Ti consente di salvare e gestire le tue schede in modo conveniente, fornendo una gamma di funzionalità che rendono facile organizzare e accedere alle tue schede salvate. Ti consente di salvare e gestire le tue schede in modo conveniente, fornendo una gamma di funzionalità che rendono facile organizzare e accedere alle tue schede salvate.
**Funzionalità** <b>Funzionalità</b>
- **Salva schede:** Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito <ul>
- **Organizza schede:** Crea collezioni e sottogruppi per organizzare le tue schede salvate <li><b>Salva schede</b>: Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito</li>
- **Cerca schede:** Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca <li><b>Organizza schede</b>: Crea collezioni e sottogruppi per organizzare le tue schede salvate</li>
- **Sincronizza tra dispositivi:** Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account <li><b>Cerca schede</b>: Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca</li>
- **Modalità scura:** Supporto per la modalità scura per un'esperienza di navigazione più confortevole <li><b>Sincronizza tra dispositivi</b>: Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account</li>
- **Personalizza:** Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze <li><b>Modalità scura</b>: Supporto per la modalità scura per un'esperienza di navigazione più confortevole</li>
<li><b>Personalizza</b>: Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze</li>
</ul>
Dai un'occhiata al nostro [post sul blog](https://at.xfox111.net/tabs-aside-3-0) riguardante tutte le nuove funzionalità e miglioramenti in Schede a parte 3.0 Dai un'occhiata al nostro <a href="https://at.xfox111.net/tabs-aside-3-0">post sul blog</a> riguardante tutte le nuove funzionalità e miglioramenti in Schede a parte 3.0
**Ehi, è un software open-source!** <b>Ehi, è un software open-source!</b>
Se sai come migliorare questa estensione, puoi controllare [il suo repository GitHub](https://github.com/xfox111/TabsAsideExtension) Se sai come migliorare questa estensione, puoi controllare <a href="https://github.com/xfox111/TabsAsideExtension">il suo repository GitHub</a>
Consulta il [registro delle modifiche](https://github.com/xfox111/TabsAsideExtension/releases/latest) Consulta il <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">registro delle modifiche</a>
+13 -11
View File
@@ -30,17 +30,19 @@ Zainspirowane funkcją z pierwszych wersji Microsoft Edge, to rozszerzenie stał
Pozwala wygodnie zapisywać i zarządzać kartami, oferując wiele funkcji, które ułatwiają organizację i dostęp do zapisanych kart. Pozwala wygodnie zapisywać i zarządzać kartami, oferując wiele funkcji, które ułatwiają organizację i dostęp do zapisanych kart.
**Funkcje** <b>Funkcje</b>
- **Zapisywanie kart:** Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później <ul>
- **Organizacja kart:** Twórz kolekcje i podgrupy, aby organizować zapisane karty <li><b>Zapisywanie kart</b>: Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później</li>
- **Wyszukiwanie kart:** Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania <li><b>Organizacja kart</b>: Twórz kolekcje i podgrupy, aby organizować zapisane karty</li>
- **Synchronizacja między urządzeniami:** Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta <li><b>Wyszukiwanie kart</b>: Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania</li>
- **Tryb ciemny:** Obsługa trybu ciemnego dla bardziej komfortowego użytkowania <li><b>Synchronizacja między urządzeniami</b>: Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta</li>
- **Personalizacja:** Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb <li><b>Tryb ciemny</b>: Obsługa trybu ciemnego dla bardziej komfortowego użytkowania</li>
<li><b>Personalizacja</b>: Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb</li>
</ul>
Odwiedź [naszego bloga](https://at.xfox111.net/tabs-aside-3-0), aby dowiedzieć się więcej o wszystkich nowych funkcjach i ulepszeniach w Odłożonych kartach 3.0 Odwiedź <a href="https://at.xfox111.net/tabs-aside-3-0">naszego bloga</a>, aby dowiedzieć się więcej o wszystkich nowych funkcjach i ulepszeniach w Odłożonych kartach 3.0
**Przy okazji, to rozszerzenie open-source!** <b>Przy okazji, to rozszerzenie open-source!</b>
Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić [jego repozytorium na GitHubie](https://github.com/xfox111/TabsAsideExtension) Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić <a href="https://github.com/xfox111/TabsAsideExtension">jego repozytorium na GitHubie</a>
[Lista zmian w najnowszej wersji](https://github.com/xfox111/TabsAsideExtension/releases/latest) <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">Lista zmian w najnowszej wersji</a>
+13 -11
View File
@@ -31,17 +31,19 @@ Originando-se do recurso original do navegador Microsoft Edge, esta extensão cr
Ela permite que você salve e gerencie suas abas de forma conveniente, oferecendo uma variedade de recursos que facilitam a organização e o acesso às abas salvas. Ela permite que você salve e gerencie suas abas de forma conveniente, oferecendo uma variedade de recursos que facilitam a organização e o acesso às abas salvas.
**Recursos** <b>Recursos</b>
- **Salvar abas:** Salve todas as suas abas abertas com um único clique e restaure-as depois <ul>
- **Organizar abas:** Crie coleções e subgrupos para organizar suas abas salvas <li><b>Salvar abas</b>: Salve todas as suas abas abertas com um único clique e restaure-as depois</li>
- **Pesquisar abas:** Encontre rapidamente as abas que você precisa usando o recurso de pesquisa <li><b>Organizar abas</b>: Crie coleções e subgrupos para organizar suas abas salvas</li>
- **Sincronizar entre dispositivos:** Acesse suas abas salvas de qualquer dispositivo com sua conta <li><b>Pesquisar abas</b>: Encontre rapidamente as abas que você precisa usando o recurso de pesquisa</li>
- **Modo escuro:** Suporte ao modo escuro para uma experiência de navegação mais confortável <li><b>Sincronizar entre dispositivos</b>: Acesse suas abas salvas de qualquer dispositivo com sua conta</li>
- **Personalizar:** Altere a aparência e o comportamento da extensão conforme suas necessidades <li><b>Modo escuro</b>: Suporte ao modo escuro para uma experiência de navegação mais confortável</li>
<li><b>Personalizar</b>: Altere a aparência e o comportamento da extensão conforme suas necessidades</li>
</ul>
Confira nossa [postagem no blog](https://at.xfox111.net/tabs-aside-3-0) sobre todos os novos recursos e melhorias do Tabs Aside 3.0 Confira nossa <a href="https://at.xfox111.net/tabs-aside-3-0">postagem no blog</a> sobre todos os novos recursos e melhorias do Tabs Aside 3.0
**Ei, é um software de código aberto!** <b>Ei, é um software de código aberto!</b>
Se você sabe como melhorar esta extensão, confira [seu repositório no GitHub](https://github.com/xfox111/TabsAsideExtension) Se você sabe como melhorar esta extensão, confira <a href="https://github.com/xfox111/TabsAsideExtension">seu repositório no GitHub</a>
Veja o [changelog das versões](https://github.com/xfox111/TabsAsideExtension/releases/latest) Veja o <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">changelog das versões</a>
+13 -11
View File
@@ -30,17 +30,19 @@ https://github.com/xfox111/TabsAsideExtension/releases/latest
Оно позволяет сохранять и управлять вашими вкладками удобным образом, предоставляя множество функций, которые упрощают организацию и доступ к сохраненным вкладкам. Оно позволяет сохранять и управлять вашими вкладками удобным образом, предоставляя множество функций, которые упрощают организацию и доступ к сохраненным вкладкам.
**Возможности** <b>Возможности</b>
- **Сохранение вкладок:** Сохраните все открытые вкладки одним кликом и восстановите их позже <ul>
- **Организация вкладок:** Создавайте коллекции и подгруппы для организации сохраненных вкладок <li><b>Сохранение вкладок</b>: Сохраните все открытые вкладки одним кликом и восстановите их позже</li>
- **Поиск вкладок:** Быстро находите нужные вкладки с помощью функции поиска <li><b>Организация вкладок</b>: Создавайте коллекции и подгруппы для организации сохраненных вкладок</li>
- **Синхронизация между устройствами:** Доступ к сохраненным вкладкам с любого устройства через ваш аккаунт <li><b>Поиск вкладок</b>: Быстро находите нужные вкладки с помощью функции поиска</li>
- **Темный режим:** Поддержка темного режима для более комфортного использования <li><b>Синхронизация между устройствами</b>: Доступ к сохраненным вкладкам с любого устройства через ваш аккаунт</li>
- **Персонализация:** Изменяйте внешний вид и поведение расширения под свои нужды <li><b>Темный режим</b>: Поддержка темного режима для более комфортного использования</li>
<li><b>Персонализация</b>: Изменяйте внешний вид и поведение расширения под свои нужды</li>
</ul>
Посетите [наш блог](https://at.xfox111.net/tabs-aside-3-0), чтобы узнать больше о всех новых функциях и улучшениях в Отложенных вкладках 3.0 по ссылке Посетите <a href="https://at.xfox111.net/tabs-aside-3-0">наш блог</a>, чтобы узнать больше о всех новых функциях и улучшениях в Отложенных вкладках 3.0 по ссылке
**Кстати это опенсорс расширение!** <b>Кстати это опенсорс расширение!</b>
Если вы знаете, как можно его улучшить, можете перейти на [страницу GitHub репозитория проекта](https://github.com/xfox111/TabsAsideExtension) Если вы знаете, как можно его улучшить, можете перейти на <a href="https://github.com/xfox111/TabsAsideExtension">страницу GitHub репозитория проекта</a>
[Список изменений последней версии](https://github.com/xfox111/TabsAsideExtension/releases/latest) <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">Список изменений последней версии</a>
+13 -11
View File
@@ -30,17 +30,19 @@ https://github.com/xfox111/TabsAsideExtension/releases/latest
Воно дозволяє зберігати та керувати вашими вкладками зручно, надаючи безліч функцій, які спрощують організацію та доступ до збережених вкладок. Воно дозволяє зберігати та керувати вашими вкладками зручно, надаючи безліч функцій, які спрощують організацію та доступ до збережених вкладок.
**Можливості** <b>Можливості</b>
- **Збереження вкладок:** Збережіть усі відкриті вкладки одним кліком і відновіть їх пізніше <ul>
- **Організація вкладок:** Створюйте колекції та підгрупи для організації збережених вкладок <li><b>Збереження вкладок</b>: Збережіть усі відкриті вкладки одним кліком і відновіть їх пізніше</li>
- **Пошук вкладок:** Швидко знаходьте потрібні вкладки за допомогою функції пошуку <li><b>Організація вкладок</b>: Створюйте колекції та підгрупи для організації збережених вкладок</li>
- **Синхронізація між пристроями:** Доступ до збережених вкладок з будь-якого пристрою через ваш обліковий запис <li><b>Пошук вкладок</b>: Швидко знаходьте потрібні вкладки за допомогою функції пошуку</li>
- **Темний режим:** Підтримка темного режиму для більш комфортного використання <li><b>Синхронізація між пристроями</b>: Доступ до збережених вкладок з будь-якого пристрою через ваш обліковий запис</li>
- **Персоналізація:** Змінюйте зовнішній вигляд і поведінку розширення під свої потреби <li><b>Темний режим</b>: Підтримка темного режиму для більш комфортного використання</li>
<li><b>Персоналізація</b>: Змінюйте зовнішній вигляд і поведінку розширення під свої потреби</li>
</ul>
Відвідайте [наш блог](https://at.xfox111.net/tabs-aside-3-0), щоб дізнатися більше про всі нові функції та покращення у Відкладених вкладках 3.0 Відвідайте <a href="https://at.xfox111.net/tabs-aside-3-0">наш блог</a>, щоб дізнатися більше про всі нові функції та покращення у Відкладених вкладках 3.0
**До речі, це опенсорс розширення!** <b>До речі, це опенсорс розширення!</b>
Якщо ви знаєте, як покращити це розширення, ви можете відвідати [його репозиторій на GitHub](https://github.com/xfox111/TabsAsideExtension) Якщо ви знаєте, як покращити це розширення, ви можете відвідати <a href="https://github.com/xfox111/TabsAsideExtension">його репозиторій на GitHub</a>
[Список змін останньої версії](https://github.com/xfox111/TabsAsideExtension/releases/latest) <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">Список змін останньої версії</a>
+13 -11
View File
@@ -30,17 +30,19 @@ https://github.com/xfox111/TabsAsideExtension/releases/latest
它允许您以方便的方式保存和管理标签,提供一系列功能,使您可以轻松组织和访问已保存的标签。 它允许您以方便的方式保存和管理标签,提供一系列功能,使您可以轻松组织和访问已保存的标签。
**功能** <b>功能</b>
- **保存标签:** 一键保存所有打开的标签,并稍后恢复 <ul>
- **组织标签:** 创建收藏和子组以组织已保存的标签 <li><b>保存标签</b>:一键保存所有打开的标签,并稍后恢复</li>
- **搜索标签:** 使用搜索功能快速找到所需的标签 <li><b>组织标签</b>:创建收藏和子组以组织已保存的标签</li>
- **跨设备同步:** 使用您的帐户从任何设备访问已保存的标签 <li><b>搜索标签</b>:使用搜索功能快速找到所需的标签</li>
- **深色模式:** 支持深色模式,提供更舒适的浏览体验 <li><b>跨设备同步</b>:使用您的帐户从任何设备访问已保存的标签</li>
- **个性化:** 更改扩展的外观和行为以满足您的需求 <li><b>深色模式</b>:支持深色模式,提供更舒适的浏览体验</li>
<li><b>个性化</b>:更改扩展的外观和行为以满足您的需求</li>
</ul>
查看我们关于 搁置的标签页 3.0 的所有新功能和改进的[博客文章](https://at.xfox111.net/tabs-aside-3-0) 查看我们关于 <a href="https://at.xfox111.net/tabs-aside-3-0">搁置的标签页 3.0</a> 的所有新功能和改进的<a href="https://at.xfox111.net/tabs-aside-3-0">博客文章</a>
**嘿,这是一个开源软件!** <b>嘿,这是一个开源软件!</b>
如果您知道如何改进此扩展,可以查看[其 GitHub 仓库](https://github.com/xfox111/TabsAsideExtension) 如果您知道如何改进此扩展,可以查看<a href="https://github.com/xfox111/TabsAsideExtension">其 GitHub 仓库</a>
查看[发布更新日志](https://github.com/xfox111/TabsAsideExtension/releases/latest) 查看<a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">发布更新日志</a>
+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
} }
} }
-11
View File
@@ -1,11 +0,0 @@
export async function closeTabsAsync(tabs: Browser.tabs.Tab[]): Promise<void>
{
if (tabs.length < 1)
return;
await browser.tabs.create({
active: true,
windowId: tabs[0].windowId
});
await browser.tabs.remove(tabs.map(i => i.id!));
}
-83
View File
@@ -1,83 +0,0 @@
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
export async function createCollectionFromTabs(tabs: Browser.tabs.Tab[]): Promise<CollectionItem>
{
const collection: CollectionItem = {
type: "collection",
timestamp: Date.now(),
items: []
};
if (tabs.length < 1)
return collection;
let tabIndex: number = 0;
if (tabs[tabIndex].pinned)
{
collection.items.push({ type: "group", pinned: true, items: [] });
for (; tabIndex < tabs.length; tabIndex++)
{
if (!tabs[tabIndex].pinned)
break;
(collection.items[0] as GroupItem).items.push({
type: "tab",
url: tabs[tabIndex].url!,
title: tabs[tabIndex].title
});
}
}
// Special case, if all tabs are in the same group, create a collection with the group title
if (tabs[0].groupId && tabs[0].groupId !== -1 &&
tabs.every(i => i.groupId === tabs[0].groupId)
)
{
const group = await browser.tabGroups.get(tabs[0].groupId);
collection.title = group.title;
collection.color = group.color;
tabs.forEach(i =>
collection.items.push({ type: "tab", url: i.url!, title: i.title })
);
return collection;
}
let activeGroup: number | null = null;
for (; tabIndex < tabs.length; tabIndex++)
{
const tab = tabs[tabIndex];
if (!tab.groupId || tab.groupId === -1)
{
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
activeGroup = null;
continue;
}
if (!activeGroup || activeGroup !== tab.groupId)
{
activeGroup = tab.groupId;
const group = await browser.tabGroups.get(activeGroup!);
collection.items.push({
type: "group",
color: group.color,
title: group.title,
items: []
});
}
(collection.items[collection.items.length - 1] as GroupItem).items.push({
type: "tab",
url: tab.url!,
title: tab.title
});
}
return collection;
}
-30
View File
@@ -1,30 +0,0 @@
import { settings } from "./settings";
export async function getTabsToSaveAsync(forceSelected: boolean = false): Promise<[Browser.tabs.Tab[], number]>
{
let tabs: Browser.tabs.Tab[] = await browser.tabs.query({
currentWindow: true,
highlighted: true
});
if (!forceSelected && tabs.length < 2)
{
const ignorePinned: boolean = await settings.ignorePinned.getValue();
tabs = await browser.tabs.query({
currentWindow: true,
pinned: ignorePinned ? false : undefined
});
}
const tabsCount: number = tabs.length;
const extension_prefix: string = browser.runtime.getURL("/");
tabs = tabs.filter(i =>
i.url
&& new URL(i.url).protocol !== "about:"
&& new URL(i.url).hostname !== "newtab"
&& !i.url!.startsWith(extension_prefix)
);
return [tabs, tabsCount - tabs.length];
}
+5 -12
View File
@@ -1,33 +1,26 @@
import { trackError } from "@/features/analytics"; import { trackError } from "@/features/analytics";
import { CollectionItem, GraphicsStorage, GroupItem } from "@/models/CollectionModels"; import { GraphicsStorage } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger, ExtensionSendMessageArgs, GetDataType, GetReturnType } from "@webext-core/messaging"; import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger } from "@webext-core/messaging";
type ProtocolMap = type ProtocolMap =
{ {
addThumbnail(data: { url: string; thumbnail: string; }): void; addThumbnail(data: { url: string; thumbnail: string; }): void;
getGraphicsCache(): GraphicsStorage; getGraphicsCache(): GraphicsStorage;
refreshCollections(): void; refreshCollections(): void;
openCollection(data: { collection: CollectionItem; targetWindow: "new" | "incognito"; }): void;
openGroup(data: { group: GroupItem; newWindow: boolean; }): void;
}; };
function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap> function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap>
{ {
const { onMessage, sendMessage, removeAllListeners }: ExtensionMessenger<ProtocolMap> = defineExtensionMessaging<ProtocolMap>(config); const { onMessage, sendMessage, removeAllListeners } = defineExtensionMessaging<ProtocolMap>(config);
return { return {
onMessage, onMessage,
removeAllListeners, removeAllListeners,
async sendMessage<TType extends keyof ProtocolMap>( sendMessage: async (type, data, args): Promise<any> =>
type: TType,
data: GetDataType<ProtocolMap[TType]>,
...args: ExtensionSendMessageArgs
): Promise<GetReturnType<ProtocolMap[TType]>>
{ {
try try
{ {
return await sendMessage(type, data, ...args); return await sendMessage(type, data, args);
} }
catch (ex) catch (ex)
{ {

Some files were not shown because too many files have changed in this diff Show More