mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-07-02 19:52:47 +03:00
Compare commits
73 Commits
next
..
v3.0.0-rc8
| Author | SHA1 | Date | |
|---|---|---|---|
| c0037cc3a7 | |||
| db30e81765 | |||
| 7e57488896 | |||
| 61d0e54e58 | |||
| a03f2ab442 | |||
| 2f6ed6cbfa | |||
| aac4ac9a7e | |||
| bc2e2489e6 | |||
| 57e87aace0 | |||
| 5f2d18f509 | |||
| 54a1e48672 | |||
| e803636c35 | |||
| c8b4ef3e15 | |||
| eeefd1feff | |||
| 53adbd4f75 | |||
| 3eed3b4b01 | |||
| 525130b7e9 | |||
| 1a274348e0 | |||
| 2c8cfa1583 | |||
| e844a68f49 | |||
| 794f6e3af0 | |||
| d85d10dc58 | |||
| 213cc84602 | |||
| 9675b65e81 | |||
| 6bce330a8f | |||
| 405f9163f2 | |||
| e498e25c57 | |||
| d07c99e3a1 | |||
| 0ff1d63cde | |||
| dfcafae2b1 | |||
| 8f4cd4198a | |||
| 1b64f65e9f | |||
| d249e07eca | |||
| 4070907240 | |||
| 9d250dc01d | |||
| 24bf0e88ca | |||
| ef94842066 | |||
| 40490aec2d | |||
| 06aca3d3ca | |||
| a6a5c236c6 | |||
| a144221e33 | |||
| e6a69980c2 | |||
| 59b0547ec6 | |||
| eed5159a56 | |||
| 7effe309dd | |||
| 6728a50056 | |||
| 0cb036c69a | |||
| 4ef336da5b | |||
| 00492ad710 | |||
| a706c3bc89 | |||
| 4ef9e2651c | |||
| bfb849fbdf | |||
| 297a6aa95c | |||
| aa2ee02c79 | |||
| 8b77159abe | |||
| f5bf0db039 | |||
| 5d4a59153a | |||
| b6be86aac9 | |||
| d872515b8b | |||
| 8693e8d563 | |||
| 9c4121ea79 | |||
| f89d036ab8 | |||
| e59782973b | |||
| 70ed16c286 | |||
| db78314a44 | |||
| a478352ca3 | |||
| 2eba532901 | |||
| 1e60b776c4 | |||
| 0c18a3de5a | |||
| 4e40742755 | |||
| 16023ac152 | |||
| 39793a38c3 | |||
| dbc8c7fd4d |
@@ -22,5 +22,5 @@
|
||||
}
|
||||
},
|
||||
|
||||
"postCreateCommand": "npm install"
|
||||
"postCreateCommand": "yarn install"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ body:
|
||||
attributes:
|
||||
label: Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: e.g. Sometimes clicking on the extension icon doesn't open the side panel
|
||||
placeholder: e.g. Sometimes when generating a password not all character sets are included
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -105,5 +105,3 @@ body:
|
||||
required: true
|
||||
- label: The provided reproduction is a minimal reproducible example of the bug.
|
||||
required: true
|
||||
- label: This issue was written in English.
|
||||
required: true
|
||||
|
||||
@@ -60,5 +60,3 @@ body:
|
||||
options:
|
||||
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
|
||||
required: true
|
||||
- label: This issue was written in English.
|
||||
required: true
|
||||
|
||||
+3
-35
@@ -16,33 +16,7 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
groups:
|
||||
deps:
|
||||
patterns:
|
||||
- "*"
|
||||
exclude-patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
- "scheduler"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
react-next:
|
||||
patterns:
|
||||
- "react"
|
||||
- "react-dom"
|
||||
- "@types/react"
|
||||
- "@types/react-dom"
|
||||
update-types:
|
||||
- major
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
@@ -52,10 +26,7 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
groups:
|
||||
actions:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
@@ -65,7 +36,4 @@ updates:
|
||||
schedule:
|
||||
interval: monthly
|
||||
rebase-strategy: disabled
|
||||
groups:
|
||||
devcontainers:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 20
|
||||
|
||||
@@ -51,19 +51,15 @@ jobs:
|
||||
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
|
||||
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
|
||||
|
||||
- run: npm install
|
||||
- run: corepack enable
|
||||
- run: yarn install
|
||||
|
||||
# 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 }}
|
||||
- run: yarn zip -b ${{ matrix.target }}
|
||||
|
||||
- name: Drop build artifacts (${{ matrix.target }})
|
||||
uses: actions/upload-artifact@main
|
||||
@@ -74,13 +70,12 @@ jobs:
|
||||
|
||||
- name: web-ext lint
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
uses: kewisch/action-web-ext@main
|
||||
uses: freaktechnik/web-ext-lint@main
|
||||
with:
|
||||
cmd: lint
|
||||
source: ./.output/firefox-mv3
|
||||
channel: listed
|
||||
extension-root: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
|
||||
- run: npm audit
|
||||
- run: yarn npm audit
|
||||
continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }}
|
||||
|
||||
publish-github:
|
||||
|
||||
@@ -52,11 +52,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@main
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,4 +83,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
extver=`jq -r ".version" package.json`
|
||||
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: dev-build-deploy/release-me@v0.18.2
|
||||
- uses: dev-build-deploy/release-me@v0.18.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: v
|
||||
|
||||
@@ -43,19 +43,15 @@ jobs:
|
||||
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
|
||||
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
|
||||
|
||||
- run: npm install
|
||||
- run: corepack enable
|
||||
- run: yarn install
|
||||
|
||||
# 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 }}
|
||||
- run: yarn zip -b ${{ matrix.target }}
|
||||
|
||||
- name: Drop artifacts (${{ matrix.target }})
|
||||
uses: actions/upload-artifact@main
|
||||
@@ -66,10 +62,9 @@ jobs:
|
||||
|
||||
- name: web-ext lint
|
||||
if: ${{ matrix.target == 'firefox' }}
|
||||
uses: kewisch/action-web-ext@main
|
||||
uses: freaktechnik/web-ext-lint@main
|
||||
with:
|
||||
cmd: lint
|
||||
source: ./.output/firefox-mv3
|
||||
channel: listed
|
||||
extension-root: ./.output/firefox-mv3
|
||||
self-hosted: false
|
||||
|
||||
- run: npm audit
|
||||
- run: yarn npm audit
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
packageExtensions:
|
||||
"@wxt-dev/module-react@*":
|
||||
peerDependencies:
|
||||
vite: "*"
|
||||
"@fluentui/react-accordion@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-avatar@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-carousel@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-color-picker@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-combobox@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-dialog@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-field@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-list@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-menu@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-nav@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-overflow@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-popover@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-swatch-picker@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-table@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-tabs@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-tag-picker@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-teaching-popover@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-toolbar@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-tree@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-alert@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-checkbox@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-components@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-drawer@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-infobutton@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-infolabel@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-input@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-persona@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-progress@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-radio@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-select@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-skeleton@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-slider@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-spinbutton@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-switch@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-tags@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-textarea@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
"@fluentui/react-search@*":
|
||||
peerDependencies:
|
||||
scheduler: "0.23.0"
|
||||
@@ -1,6 +1,6 @@
|
||||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
+2
-35
@@ -6,7 +6,7 @@
|
||||
- Thumbnails of saved tabs
|
||||
3. This extension uses Google Analytics to collect usage statistics and improve the extension.
|
||||
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
|
||||
- Operating system name and version
|
||||
- System architecture
|
||||
@@ -14,40 +14,7 @@
|
||||
- Extension language
|
||||
- User settings
|
||||
- Number of saved collections
|
||||
- Events, related to user's actions:
|
||||
- `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
|
||||
- Action identifiers (e.g. "page_view", "extension_installed", "item_created", etc.)
|
||||
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.
|
||||
6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
|
||||
|
||||
@@ -37,7 +37,7 @@ Check out our [latest blog post](https://at.xfox111.net/tabs-aside-3-0) regardin
|
||||
|
||||
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin)
|
||||
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd)
|
||||
- [Firefox Add-ons](https://addons.mozilla.org/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)
|
||||
|
||||
### 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
|
||||
[](https://github.com/xfox111/TabsAsideExtension/issues)
|
||||
[](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yml)
|
||||
[](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yaml)
|
||||
[](https://github.com/xfox111/TabsAsideExtension)
|
||||
|
||||
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
|
||||
[](https://github.com/xfox111)
|
||||
[](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)
|
||||
|
||||
@@ -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);
|
||||
+2
-3
@@ -13,13 +13,12 @@ export const githubLinks =
|
||||
repo: githubLink(),
|
||||
release: githubLink(`releases/tag/v${Package.version}`),
|
||||
license: githubLink("blob/main/LICENSE"),
|
||||
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations"),
|
||||
privacy: githubLink("blob/main/PRIVACY.md")
|
||||
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations")
|
||||
};
|
||||
|
||||
export const storeLink: string =
|
||||
import.meta.env.FIREFOX
|
||||
? "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://chromewebstore.google.com/detail/tabs-aside/mgmjbodjgijnebfgohlnjkegdpbdjgin";
|
||||
|
||||
+46
-150
@@ -1,32 +1,24 @@
|
||||
import { track, trackError } from "@/features/analytics";
|
||||
import { collectionCount, getCollections, saveCollections, thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||
import { collectionStorage } from "@/features/collectionStorage/utils/collectionStorage";
|
||||
import getCollectionsFromCloud from "@/features/collectionStorage/utils/getCollectionsFromCloud";
|
||||
import getCollectionsFromLocal from "@/features/collectionStorage/utils/getCollectionsFromLocal";
|
||||
import { collectionCount, getCollections, saveCollections } from "@/features/collectionStorage";
|
||||
import { migrateStorage } from "@/features/migration";
|
||||
import { setSettingsReviewNeeded } from "@/features/settingsReview/utils";
|
||||
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
|
||||
import { SettingsValue } from "@/hooks/useSettings";
|
||||
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
import { closeTabsAsync } from "@/utils/closeTabsAsync";
|
||||
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import { onMessage, sendMessage } from "@/utils/messaging";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import { settings } from "@/utils/settings";
|
||||
import watchTabSelection from "@/utils/watchTabSelection";
|
||||
import { RemoveListenerCallback } from "@webext-core/messaging";
|
||||
import { Unwatch } from "wxt/utils/storage";
|
||||
import { openCollection, openGroup } from "./sidepanel/utils/opener";
|
||||
import { Tabs, Windows } from "wxt/browser";
|
||||
import { Unwatch } from "wxt/storage";
|
||||
|
||||
export default defineBackground(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const logger = getLogger("background");
|
||||
let graphicsCache: GraphicsStorage = {};
|
||||
const graphicsCache: GraphicsStorage = {};
|
||||
let listLocation: SettingsValue<"listLocation"> = "sidebar";
|
||||
|
||||
logger("Background script started");
|
||||
@@ -41,42 +33,26 @@ export default defineBackground(() =>
|
||||
logger("onInstalled", reason, previousVersion);
|
||||
track("extension_installed", { reason, previousVersion: previousVersion ?? "none" });
|
||||
|
||||
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
|
||||
const cumulative: number = major * 10000 + minor * 100 + patch;
|
||||
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
|
||||
|
||||
await setSettingsReviewNeeded(reason, previousVersion);
|
||||
|
||||
if (reason === "update" && cumulative < 30000) // < 3.0.0
|
||||
if (reason === "update" && previousMajor < 3)
|
||||
{
|
||||
await migrateStorage();
|
||||
await showWelcomeDialog.setValue(true);
|
||||
browser.runtime.reload();
|
||||
}
|
||||
});
|
||||
|
||||
if (reason === "update" && cumulative >= 30000 && cumulative < 30200) // >= 3.0.0 && < 3.2.0
|
||||
{
|
||||
// Merge cloud and local storage if they are out of sync
|
||||
const localTimestamp: number = await collectionStorage.localLastUpdated.getValue();
|
||||
const syncTimestamp: number = await collectionStorage.syncLastUpdated.getValue();
|
||||
browser.tabs.onUpdated.addListener((_, __, tab) =>
|
||||
{
|
||||
if (!tab.url)
|
||||
return;
|
||||
|
||||
if (localTimestamp === syncTimestamp)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
const localCollections: CollectionItem[] = await getCollectionsFromLocal();
|
||||
const cloudCollections: CollectionItem[] = await getCollectionsFromCloud();
|
||||
const mergedCollections: CollectionItem[] = [...cloudCollections, ...localCollections];
|
||||
|
||||
await saveCollections(mergedCollections, true, graphicsCache);
|
||||
}
|
||||
catch (ex)
|
||||
{
|
||||
logger("Failed to merge cloud and local storage during update");
|
||||
trackError("cloud_sync_merge_error", ex as Error);
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
graphicsCache[tab.url] = {
|
||||
preview: graphicsCache[tab.url]?.preview,
|
||||
capture: graphicsCache[tab.url]?.capture,
|
||||
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
|
||||
};
|
||||
});
|
||||
|
||||
browser.commands.onCommand.addListener(
|
||||
@@ -84,33 +60,20 @@ export default defineBackground(() =>
|
||||
);
|
||||
|
||||
onMessage("getGraphicsCache", () => graphicsCache);
|
||||
onMessage("refreshCollections", () => { });
|
||||
|
||||
if (import.meta.env.FIREFOX)
|
||||
onMessage("addThumbnail", ({ data }) =>
|
||||
{
|
||||
onMessage("openCollection", ({ data }) => openCollection(data.collection, data.targetWindow));
|
||||
onMessage("openGroup", ({ data }) => openGroup(data.group, data.newWindow));
|
||||
}
|
||||
graphicsCache[data.url] = {
|
||||
preview: data.thumbnail,
|
||||
capture: graphicsCache[data.url]?.capture,
|
||||
icon: graphicsCache[data.url]?.icon
|
||||
};
|
||||
});
|
||||
onMessage("refreshCollections", () => {});
|
||||
|
||||
setupTabCaputre();
|
||||
async function setupTabCaputre(): Promise<void>
|
||||
{
|
||||
let unwatchAddThumbnail: RemoveListenerCallback | null = null;
|
||||
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> =>
|
||||
const tryCaptureTab = async (tab: Tabs.Tab): Promise<void> =>
|
||||
{
|
||||
if (!tab.url || tab.status !== "complete" || !tab.active)
|
||||
return;
|
||||
@@ -122,7 +85,7 @@ export default defineBackground(() =>
|
||||
{
|
||||
// We use chrome here because polyfill throws uncatchable errors for some reason
|
||||
// It's a compatible API anyway
|
||||
const capture: string = await browser.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
|
||||
const capture: string = await chrome.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
|
||||
|
||||
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"] });
|
||||
|
||||
if (captureThumbnails)
|
||||
{
|
||||
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);
|
||||
browser.tabs.query({ active: true })
|
||||
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
setupContextMenu();
|
||||
@@ -286,7 +199,6 @@ export default defineBackground(() =>
|
||||
};
|
||||
|
||||
const toggleSidebarFirefox = async (): Promise<void> =>
|
||||
// @ts-expect-error Firefox-only API
|
||||
await browser.sidebarAction.toggle();
|
||||
|
||||
const updateButton = async (action: SettingsValue<"contextAction">): Promise<void> =>
|
||||
@@ -303,7 +215,7 @@ export default defineBackground(() =>
|
||||
unwatchActionTitle?.();
|
||||
|
||||
if (!import.meta.env.FIREFOX)
|
||||
await browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
|
||||
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
|
||||
|
||||
// Setup new behavior
|
||||
if (action === "action")
|
||||
@@ -322,7 +234,7 @@ export default defineBackground(() =>
|
||||
if (import.meta.env.FIREFOX)
|
||||
browser.action.onClicked.addListener(toggleSidebarFirefox);
|
||||
else
|
||||
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
|
||||
}
|
||||
else if (location !== "popup")
|
||||
browser.action.onClicked.addListener(openCollectionsInTab);
|
||||
@@ -341,17 +253,17 @@ export default defineBackground(() =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (openWindow.incognito || openWindow.type !== "normal")
|
||||
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"));
|
||||
|
||||
const targetTab: Browser.tabs.Tab | undefined = activeTabs.find(tab => tab.pinned);
|
||||
const targetTab: Tabs.Tab | undefined = activeTabs.find(tab => tab.pinned);
|
||||
|
||||
if (!targetTab)
|
||||
await browser.tabs.create({
|
||||
@@ -361,7 +273,7 @@ export default defineBackground(() =>
|
||||
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)
|
||||
await browser.tabs.remove(tabsToClose.map(tab => tab.id!));
|
||||
@@ -373,7 +285,7 @@ export default defineBackground(() =>
|
||||
logger("updateView", viewLocation);
|
||||
|
||||
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")
|
||||
});
|
||||
await browser.tabs.remove(tabs.map(tab => tab.id!));
|
||||
@@ -383,12 +295,11 @@ export default defineBackground(() =>
|
||||
});
|
||||
|
||||
if (import.meta.env.FIREFOX)
|
||||
// @ts-expect-error Firefox-only API
|
||||
await browser.sidebarAction.setPanel({
|
||||
panel: viewLocation === "sidebar" ? browser.runtime.getURL("/sidepanel.html") : ""
|
||||
});
|
||||
else
|
||||
await browser.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
|
||||
await chrome.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
|
||||
|
||||
if (viewLocation === "pinned")
|
||||
{
|
||||
@@ -419,10 +330,9 @@ export default defineBackground(() =>
|
||||
if (view === "sidebar")
|
||||
{
|
||||
if (import.meta.env.FIREFOX)
|
||||
// @ts-expect-error Firefox-only API
|
||||
browser.sidebarAction.open();
|
||||
else
|
||||
browser.sidePanel.open({ windowId });
|
||||
chrome.sidePanel.open({ windowId });
|
||||
}
|
||||
else
|
||||
browser.action.openPopup();
|
||||
@@ -432,11 +342,11 @@ export default defineBackground(() =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 =>
|
||||
!window.incognito &&
|
||||
@@ -445,7 +355,7 @@ export default defineBackground(() =>
|
||||
|
||||
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")
|
||||
)!;
|
||||
|
||||
@@ -462,7 +372,7 @@ export default defineBackground(() =>
|
||||
}
|
||||
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")
|
||||
);
|
||||
|
||||
@@ -481,28 +391,14 @@ export default defineBackground(() =>
|
||||
{
|
||||
logger("saveTabs", closeAfterSave);
|
||||
|
||||
const [tabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (tabs.length < 1)
|
||||
{
|
||||
await sendPartialSaveNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
const collection: CollectionItem = await createCollectionFromTabs(tabs);
|
||||
const collection: CollectionItem = await saveTabsToCollection(closeAfterSave);
|
||||
const [savedCollections, cloudIssue] = await getCollections();
|
||||
const newList = [collection, ...savedCollections];
|
||||
|
||||
await saveCollections(newList, cloudIssue === null, graphicsCache);
|
||||
|
||||
track(closeAfterSave ? "set_aside" : "save");
|
||||
sendMessage("refreshCollections", undefined);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
|
||||
if (closeAfterSave)
|
||||
await closeTabsAsync(tabs);
|
||||
|
||||
if (await settings.notifyOnSave.getValue())
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.tabs_saved.title"),
|
||||
|
||||
@@ -4,7 +4,11 @@ import { sendMessage } from "@/utils/messaging";
|
||||
// This content script is injected into each browser tab.
|
||||
// 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");
|
||||
|
||||
@@ -34,18 +34,5 @@ export const useOptionsStyles = makeStyles({
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: tokens.spacingHorizontalS
|
||||
},
|
||||
group:
|
||||
{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: tokens.spacingVerticalSNudge
|
||||
},
|
||||
img:
|
||||
{
|
||||
height: "100px",
|
||||
flexGrow: 1,
|
||||
alignSelf: "flex-end"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
|
||||
import { bskyLink, buyMeACoffeeLink, githubLinks, storeLink, websiteLink } from "@/data/links";
|
||||
import { useBmcStyles } from "@/hooks/useBmcStyles";
|
||||
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 { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||
import Package from "@/package.json";
|
||||
@@ -19,6 +19,24 @@ export default function AboutSection(): React.ReactElement
|
||||
<sup><Caption1> v{ Package.version }</Caption1></sup>
|
||||
</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 }>
|
||||
<Button
|
||||
as="a" { ...extLink(storeLink) }
|
||||
@@ -35,27 +53,6 @@ export default function AboutSection(): React.ReactElement
|
||||
{ i18n.t("common.cta.sponsor") }
|
||||
</Button>
|
||||
</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 { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
|
||||
import { KeyCommand20Regular } from "@fluentui/react-icons";
|
||||
@@ -14,25 +13,9 @@ export default function GeneralSection(): React.ReactElement
|
||||
const [dismissOnLoad, setDismissOnLoad] = useSettings("dismissOnLoad");
|
||||
const [listLocation, setListLocation] = useSettings("listLocation");
|
||||
const [contextAction, setContextAction] = useSettings("contextAction");
|
||||
const [showPartialSaveNotification, setShowPartialSaveNotification] = useSettings("showPartialSaveNotification");
|
||||
|
||||
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
|
||||
|
||||
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> =>
|
||||
browser.tabs.create({
|
||||
url: "chrome://extensions/shortcuts",
|
||||
@@ -45,7 +28,6 @@ export default function GeneralSection(): React.ReactElement
|
||||
setContextAction("open");
|
||||
|
||||
if (import.meta.env.FIREFOX && e.optionValue !== "sidebar")
|
||||
// @ts-expect-error Firefox-only API
|
||||
browser.sidebarAction.close();
|
||||
|
||||
setListLocation(e.optionValue as ListLocationType);
|
||||
@@ -74,20 +56,10 @@ export default function GeneralSection(): React.ReactElement
|
||||
label={ i18n.t("options_page.general.options.show_notification") }
|
||||
checked={ notifyOnSave ?? false }
|
||||
onChange={ (_, e) => setNotifyOnSave(e.checked as boolean) } />
|
||||
<Checkbox
|
||||
label={ i18n.t("options_page.general.options.show_partial_save_notification") }
|
||||
checked={ showPartialSaveNotification ?? false }
|
||||
onChange={ (_, e) => setShowPartialSaveNotification(e.checked as boolean) } />
|
||||
<Checkbox
|
||||
label={ i18n.t("options_page.general.options.unload_tabs") }
|
||||
checked={ dismissOnLoad ?? false }
|
||||
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>
|
||||
|
||||
<Field label={ i18n.t("options_page.general.options.list_locations.title") }>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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 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 { Unwatch } from "wxt/utils/storage";
|
||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||
import exportData from "../utils/exportData";
|
||||
import importData from "../utils/importData";
|
||||
@@ -14,7 +13,6 @@ export default function StorageSection(): React.ReactElement
|
||||
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
|
||||
const [importResult, setImportResult] = useState<boolean | null>(null);
|
||||
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
|
||||
const [isThumbnailCaptureEnabled, setThumbnailCaptureEnabled] = useState<boolean | null>(null);
|
||||
|
||||
const dialog = useDialog();
|
||||
const cls = useOptionsStyles();
|
||||
@@ -22,35 +20,10 @@ export default function StorageSection(): React.ReactElement
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
thumbnailCaptureEnabled.getValue().then(setThumbnailCaptureEnabled);
|
||||
cloudDisabled.getValue().then(setCloudDisabled);
|
||||
|
||||
const unwatchCloud: Unwatch = cloudDisabled.watch(setCloudDisabled);
|
||||
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setThumbnailCaptureEnabled);
|
||||
|
||||
return () =>
|
||||
{
|
||||
unwatchCloud();
|
||||
unwatchThumbnails();
|
||||
};
|
||||
return cloudDisabled.watch(setCloudDisabled);
|
||||
}, []);
|
||||
|
||||
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 =>
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("options_page.storage.import_prompt.title"),
|
||||
@@ -78,29 +51,6 @@ export default function StorageSection(): React.ReactElement
|
||||
|
||||
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 &&
|
||||
<Field
|
||||
label={ i18n.t("options_page.storage.capacity.title") }
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import App from "@/App.tsx";
|
||||
import "@/assets/global.css";
|
||||
import { trackPage } from "@/features/analytics";
|
||||
import { Tab, TabList } from "@fluentui/react-components";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
|
||||
@@ -15,7 +14,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
</App>
|
||||
);
|
||||
|
||||
trackPage("options_page");
|
||||
analytics.page("options_page");
|
||||
|
||||
function OptionsPage(): React.ReactElement
|
||||
{
|
||||
|
||||
@@ -37,12 +37,7 @@ export default async function importData(): Promise<boolean | null>
|
||||
await browser.storage.local.set(data.local);
|
||||
|
||||
if (data.sync)
|
||||
{
|
||||
if (import.meta.env.FIREFOX && data.sync.contextAction === "context")
|
||||
data.sync.contextAction = "open";
|
||||
|
||||
await browser.storage.sync.set(data.sync);
|
||||
}
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
|
||||
@@ -19,11 +19,6 @@ export const useStyles_CollectionView = makeStyles({
|
||||
"&:hover":
|
||||
{
|
||||
boxShadow: tokens.shadow4
|
||||
},
|
||||
|
||||
"&:not(:focus-within) .compact":
|
||||
{
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
color:
|
||||
|
||||
@@ -12,12 +12,7 @@ import { useStyles_CollectionView } from "./CollectionView.styles";
|
||||
import GroupView from "./GroupView";
|
||||
import TabView from "./TabView";
|
||||
|
||||
export default function CollectionView({
|
||||
collection,
|
||||
index: collectionIndex,
|
||||
dragOverlay,
|
||||
compact
|
||||
}: CollectionViewProps): ReactElement
|
||||
export default function CollectionView({ collection, index: collectionIndex, dragOverlay }: CollectionViewProps): ReactElement
|
||||
{
|
||||
const { tilesView } = useCollections();
|
||||
const {
|
||||
@@ -58,12 +53,12 @@ export default function CollectionView({
|
||||
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
|
||||
<>
|
||||
{ collection.items.length < 1 ?
|
||||
<div className={ mergeClasses(cls.empty, compact === true && "compact") }>
|
||||
<div className={ cls.empty }>
|
||||
<CollectionsRegular fontSize={ 32 } />
|
||||
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
|
||||
</div>
|
||||
:
|
||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList, compact === true && "compact") }>
|
||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
|
||||
<SortableContext
|
||||
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
|
||||
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
|
||||
@@ -71,12 +66,9 @@ export default function CollectionView({
|
||||
{ collection.items.map((i, index) =>
|
||||
i.type === "group" ?
|
||||
<GroupView
|
||||
key={ index } group={ i } indices={ [collectionIndex, index] }
|
||||
collectionId={ collection.timestamp } />
|
||||
key={ index } group={ i } indices={ [collectionIndex, index] } />
|
||||
:
|
||||
<TabView
|
||||
key={ index } tab={ i } indices={ [collectionIndex, index] }
|
||||
collectionId={ collection.timestamp } />
|
||||
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
|
||||
) }
|
||||
</SortableContext>
|
||||
</div>
|
||||
@@ -93,5 +85,4 @@ export type CollectionViewProps =
|
||||
collection: CollectionItem;
|
||||
index: number;
|
||||
dragOverlay?: boolean;
|
||||
compact?: boolean | null;
|
||||
};
|
||||
|
||||
@@ -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.collection?.color :
|
||||
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 colorCls = useGroupColors();
|
||||
const horizontalNavigationAttributes = fui.useArrowNavigationGroup({ axis: "horizontal" });
|
||||
|
||||
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
|
||||
{
|
||||
e.preventDefault();
|
||||
handleSave();
|
||||
};
|
||||
|
||||
const handleSave = () =>
|
||||
{
|
||||
@@ -65,80 +58,78 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
||||
|
||||
return (
|
||||
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
|
||||
<form onSubmit={ onSubmit }>
|
||||
<fui.DialogBody>
|
||||
<fui.DialogTitle>
|
||||
{
|
||||
props.type === "collection" ?
|
||||
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
|
||||
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
|
||||
}
|
||||
</fui.DialogTitle>
|
||||
<fui.DialogBody>
|
||||
<fui.DialogTitle>
|
||||
{
|
||||
props.type === "collection" ?
|
||||
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
|
||||
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
|
||||
}
|
||||
</fui.DialogTitle>
|
||||
|
||||
<fui.DialogContent>
|
||||
<div className={ cls.content }>
|
||||
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
|
||||
<fui.Input
|
||||
contentBefore={ <Rename20Regular /> }
|
||||
disabled={ color === "pinned" }
|
||||
placeholder={
|
||||
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
|
||||
}
|
||||
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
|
||||
onChange={ (_, e) => setTitle(e.value) } />
|
||||
</fui.Field>
|
||||
<fui.Field label={ i18n.t("dialogs.edit.color") }>
|
||||
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
|
||||
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
|
||||
<fui.ToggleButton
|
||||
checked={ color === "pinned" }
|
||||
onClick={ () => setColor("pinned") }
|
||||
icon={ <Pin20Filled /> }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t("groups.pinned") }
|
||||
</fui.ToggleButton>
|
||||
}
|
||||
{ props.type === "collection" &&
|
||||
<fui.ToggleButton
|
||||
checked={ color === undefined }
|
||||
onClick={ () => setColor(undefined) }
|
||||
icon={ <CircleOff20Regular /> }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t("colors.none") }
|
||||
</fui.ToggleButton>
|
||||
}
|
||||
{ Object.keys(colorCls).map(i =>
|
||||
<fui.ToggleButton
|
||||
checked={ color === i }
|
||||
onClick={ () => setColor(i as `${Browser.tabGroups.Color}`) }
|
||||
className={ fui.mergeClasses(cls.colorButton, colorCls[i as `${Browser.tabGroups.Color}`]) }
|
||||
icon={ {
|
||||
className: cls.colorButton_icon,
|
||||
children: <Circle20Filled />
|
||||
} }
|
||||
key={ i }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) }
|
||||
</fui.ToggleButton>
|
||||
) }
|
||||
</div>
|
||||
</fui.Field>
|
||||
</div>
|
||||
</fui.DialogContent>
|
||||
<fui.DialogContent>
|
||||
<form className={ cls.content }>
|
||||
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
|
||||
<fui.Input
|
||||
contentBefore={ <Rename20Regular /> }
|
||||
disabled={ color === "pinned" }
|
||||
placeholder={
|
||||
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
|
||||
}
|
||||
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
|
||||
onChange={ (_, e) => setTitle(e.value) } />
|
||||
</fui.Field>
|
||||
<fui.Field label="Color">
|
||||
<div className={ cls.colorPicker }>
|
||||
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
|
||||
<fui.ToggleButton
|
||||
checked={ color === "pinned" }
|
||||
onClick={ () => setColor("pinned") }
|
||||
icon={ <Pin20Filled /> }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t("groups.pinned") }
|
||||
</fui.ToggleButton>
|
||||
}
|
||||
{ props.type === "collection" &&
|
||||
<fui.ToggleButton
|
||||
checked={ color === undefined }
|
||||
onClick={ () => setColor(undefined) }
|
||||
icon={ <CircleOff20Regular /> }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t("colors.none") }
|
||||
</fui.ToggleButton>
|
||||
}
|
||||
{ Object.keys(colorCls).map(i =>
|
||||
<fui.ToggleButton
|
||||
checked={ color === i }
|
||||
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
|
||||
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
|
||||
icon={ {
|
||||
className: cls.colorButton_icon,
|
||||
children: <Circle20Filled />
|
||||
} }
|
||||
key={ i }
|
||||
shape="circular"
|
||||
>
|
||||
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
||||
</fui.ToggleButton>
|
||||
) }
|
||||
</div>
|
||||
</fui.Field>
|
||||
</form>
|
||||
</fui.DialogContent>
|
||||
|
||||
<fui.DialogActions>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="primary" as="button" type="submit">{ i18n.t("common.actions.save") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
</fui.DialogActions>
|
||||
</fui.DialogBody>
|
||||
</form>
|
||||
<fui.DialogActions>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="primary" onClick={ handleSave }>{ i18n.t("common.actions.save") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
<fui.DialogTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
|
||||
</fui.DialogTrigger>
|
||||
</fui.DialogActions>
|
||||
</fui.DialogBody>
|
||||
</fui.DialogSurface>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import GroupMoreMenu from "./collections/GroupMoreMenu";
|
||||
import { useStyles_GroupView } from "./GroupView.styles";
|
||||
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 { tilesView } = useCollections();
|
||||
@@ -101,9 +101,7 @@ export default function GroupView({ group, indices, dragOverlay, collectionId }:
|
||||
strategy={ !tilesView ? verticalListSortingStrategy : horizontalListSortingStrategy }
|
||||
>
|
||||
{ group.items.map((i, index) =>
|
||||
<TabView
|
||||
key={ index } tab={ i } indices={ [...indices, index] }
|
||||
collectionId={ collectionId } />
|
||||
<TabView key={ index } tab={ i } indices={ [...indices, index] } />
|
||||
) }
|
||||
</SortableContext>
|
||||
</div>
|
||||
@@ -119,5 +117,4 @@ export type GroupViewProps =
|
||||
group: GroupItem;
|
||||
indices: number[];
|
||||
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;
|
||||
};
|
||||
@@ -4,17 +4,16 @@ import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import useDndItem from "@/entrypoints/sidepanel/hooks/useDndItem";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components";
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import { Button, Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-components";
|
||||
import { Dismiss20Regular } from "@fluentui/react-icons";
|
||||
import { MouseEventHandler, ReactElement } from "react";
|
||||
import { useStyles_TabView } from "./TabView.styles";
|
||||
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 {
|
||||
setNodeRef, setActivatorNodeRef,
|
||||
@@ -27,8 +26,11 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
|
||||
|
||||
const cls = useStyles_TabView();
|
||||
|
||||
const handleDelete = (): void =>
|
||||
const handleDelete: MouseEventHandler<HTMLButtonElement> = (args) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
args.stopPropagation();
|
||||
|
||||
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
|
||||
|
||||
if (deletePrompt)
|
||||
@@ -43,26 +45,6 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
|
||||
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) =>
|
||||
{
|
||||
args.preventDefault();
|
||||
@@ -109,10 +91,12 @@ export default function TabView({ tab, indices, dragOverlay, collectionId }: Tab
|
||||
</Caption1>
|
||||
</Tooltip>
|
||||
|
||||
<TabMoreButton
|
||||
className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) }
|
||||
onEdit={ handleEdit }
|
||||
onDelete={ handleDelete } />
|
||||
<Tooltip relationship="label" content={ i18n.t("tabs.delete") }>
|
||||
<Button
|
||||
className={ mergeClasses(cls.deleteButton, showToolbar === true && cls.showDeleteButton) }
|
||||
appearance="subtle" icon={ <Dismiss20Regular /> }
|
||||
onClick={ handleDelete } />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
@@ -123,5 +107,4 @@ export type TabViewProps =
|
||||
tab: TabItem;
|
||||
indices: number[];
|
||||
dragOverlay?: boolean;
|
||||
collectionId: number;
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
|
||||
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 { Add20Filled, Add20Regular, bundleIcon, EyeOff16Regular } from "@fluentui/react-icons";
|
||||
import { Add20Filled, Add20Regular, bundleIcon } from "@fluentui/react-icons";
|
||||
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||
import CollectionMoreButton from "./CollectionMoreButton";
|
||||
import OpenCollectionButton from "./OpenCollectionButton";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
|
||||
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
|
||||
{
|
||||
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||
const { updateCollection } = useCollections();
|
||||
const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
|
||||
@@ -23,16 +23,10 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
|
||||
const handleAddSelected = async () =>
|
||||
{
|
||||
const [newTabs, skipCount] = await getTabsToSaveAsync(true);
|
||||
|
||||
if (newTabs.length > 0)
|
||||
await updateCollection({
|
||||
...collection,
|
||||
items: [...collection.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
|
||||
}, collection.timestamp);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
const newTabs: (TabItem | GroupItem)[] = isTab ?
|
||||
(await saveTabsToCollection(false)).items :
|
||||
await getSelectedTabs();
|
||||
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collection.timestamp);
|
||||
};
|
||||
|
||||
const cls = useStyles();
|
||||
@@ -45,12 +39,9 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
content={ getCollectionTitle(collection) }
|
||||
positioning="above-start"
|
||||
>
|
||||
<div className={ cls.titleContainer }>
|
||||
{ collection.hidden && <EyeOff16Regular /> }
|
||||
<Subtitle2 truncate wrap={ false } className={ cls.titleText }>
|
||||
{ getCollectionTitle(collection) }
|
||||
</Subtitle2>
|
||||
</div>
|
||||
<Subtitle2 truncate wrap={ false } className={ cls.titleText }>
|
||||
{ getCollectionTitle(collection) }
|
||||
</Subtitle2>
|
||||
</Tooltip>
|
||||
|
||||
<Caption1>
|
||||
@@ -68,7 +59,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
||||
>
|
||||
{ tabCount < 1 ?
|
||||
<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>
|
||||
:
|
||||
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
|
||||
@@ -115,11 +106,5 @@ const useStyles = makeStyles({
|
||||
showToolbar:
|
||||
{
|
||||
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 DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular);
|
||||
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();
|
||||
|
||||
@@ -41,11 +39,6 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
|
||||
removeItem(collection.timestamp);
|
||||
};
|
||||
|
||||
const toggleHidden = () =>
|
||||
{
|
||||
updateCollection({ ...collection, hidden: !collection.hidden }, collection.timestamp);
|
||||
};
|
||||
|
||||
const handleEdit = () =>
|
||||
dialog.pushCustom(
|
||||
<EditDialog
|
||||
@@ -89,9 +82,6 @@ export default function CollectionMoreButton({ onAddSelected, onOpenChange }: Co
|
||||
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
|
||||
{ i18n.t("collections.menu.edit") }
|
||||
</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 }>
|
||||
{ i18n.t("collections.menu.delete") }
|
||||
</MenuItem>
|
||||
|
||||
@@ -3,21 +3,20 @@ import EditDialog from "@/entrypoints/sidepanel/components/EditDialog";
|
||||
import CollectionContext, { CollectionContextType } from "@/entrypoints/sidepanel/contexts/CollectionContext";
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import GroupContext, { GroupContextType } from "@/entrypoints/sidepanel/contexts/GroupContext";
|
||||
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
|
||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import { sendMessage } from "@/utils/messaging";
|
||||
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
import { ReactElement } from "react";
|
||||
import { openGroup } from "../../utils/opener";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
|
||||
export default function GroupMoreMenu(): ReactElement
|
||||
{
|
||||
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 { hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
const [deletePrompt] = useSettings("deletePrompt");
|
||||
@@ -57,26 +56,12 @@ export default function GroupMoreMenu(): ReactElement
|
||||
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 [newTabs, skipCount] = await getTabsToSaveAsync(true);
|
||||
|
||||
if (newTabs.length > 0)
|
||||
await updateGroup({
|
||||
...group,
|
||||
items: [...group.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
|
||||
}, collection.timestamp, indices[1]);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
const newTabs: TabItem[] = isTab ?
|
||||
(await saveTabsToCollection(false)).items.flatMap(i => i.type === "tab" ? i : i.items) :
|
||||
await getSelectedTabs();
|
||||
updateGroup({ ...group, items: [...group.items, ...newTabs] }, collection.timestamp, indices[1]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -90,13 +75,13 @@ export default function GroupMoreMenu(): ReactElement
|
||||
<MenuPopover>
|
||||
<MenuList>
|
||||
{ group.items.length > 0 &&
|
||||
<MenuItem icon={ <NewWindowIcon /> } onClick={ openGroupInNewWindow }>
|
||||
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => openGroup(group, true) }>
|
||||
{ i18n.t("groups.menu.new_window") }
|
||||
</MenuItem>
|
||||
}
|
||||
|
||||
<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 icon={ <EditIcon /> } onClick={ handleEdit }>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import useSettings from "@/hooks/useSettings";
|
||||
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 * as ic from "@fluentui/react-icons";
|
||||
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
||||
@@ -11,7 +10,6 @@ import { openCollection } from "../../utils/opener";
|
||||
export default function OpenCollectionButton({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement
|
||||
{
|
||||
const [defaultAction] = useSettings("defaultRestoreAction");
|
||||
const [listLocation] = useSettings("listLocation");
|
||||
const { removeItem } = useCollections();
|
||||
const dialog = useDialog();
|
||||
const { collection } = useContext<CollectionContextType>(CollectionContext);
|
||||
@@ -24,12 +22,7 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
|
||||
const handleIncognito = async () =>
|
||||
{
|
||||
if (await browser.extension.isAllowedIncognitoAccess())
|
||||
{
|
||||
if (import.meta.env.FIREFOX && listLocation === "popup")
|
||||
sendMessage("openCollection", { collection, targetWindow: "incognito" });
|
||||
else
|
||||
openCollection(collection, "incognito");
|
||||
}
|
||||
openCollection(collection, "incognito");
|
||||
else
|
||||
dialog.pushPrompt({
|
||||
title: i18n.t("collections.incognito_check.title"),
|
||||
@@ -52,9 +45,7 @@ export default function OpenCollectionButton({ onOpenChange }: OpenCollectionBut
|
||||
};
|
||||
|
||||
const handleOpen = (mode: "current" | "new") =>
|
||||
import.meta.env.FIREFOX && listLocation === "popup" && mode === "new" ?
|
||||
() => sendMessage("openCollection", { collection, targetWindow: "new" }) :
|
||||
() => openCollection(collection, mode);
|
||||
() => openCollection(collection, mode);
|
||||
|
||||
const handleRestore = async () =>
|
||||
{
|
||||
|
||||
@@ -43,12 +43,12 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
|
||||
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]);
|
||||
|
||||
@@ -59,34 +59,34 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
|
||||
else
|
||||
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);
|
||||
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);
|
||||
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 group = collections[collectionIndex].items[groupIndex] as GroupItem;
|
||||
collections[collectionIndex].items.splice(groupIndex, 1, ...group.items);
|
||||
await updateStorage(collections);
|
||||
updateStorage(collections);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -110,12 +110,12 @@ export type CollectionsContextType =
|
||||
tilesView: boolean;
|
||||
|
||||
refreshCollections: () => Promise<void>;
|
||||
addCollection: (collection: CollectionItem) => Promise<void>;
|
||||
addCollection: (collection: CollectionItem) => void;
|
||||
|
||||
updateCollections: (collections: CollectionItem[]) => Promise<void>;
|
||||
updateCollection: (collection: CollectionItem, id: number) => Promise<void>;
|
||||
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => Promise<void>;
|
||||
ungroup: (collectionId: number, groupIndex: number) => Promise<void>;
|
||||
updateCollections: (collections: CollectionItem[]) => void;
|
||||
updateCollection: (collection: CollectionItem, id: number) => void;
|
||||
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => 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))"
|
||||
}
|
||||
},
|
||||
compactList:
|
||||
{
|
||||
alignItems: "baseline"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -18,10 +18,10 @@ import CollectionContext from "../../contexts/CollectionContext";
|
||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||
import applyReorder from "../../utils/dnd/applyReorder";
|
||||
import { collisionDetector } from "../../utils/dnd/collisionDetector";
|
||||
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
|
||||
import { useStyles_CollectionListView } from "./CollectionListView.styles";
|
||||
import SearchBar from "./SearchBar";
|
||||
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
|
||||
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
|
||||
|
||||
export default function CollectionListView(): ReactElement
|
||||
{
|
||||
@@ -30,19 +30,17 @@ export default function CollectionListView(): ReactElement
|
||||
const [sortMode, setSortMode] = useSettings("sortMode");
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [colors, setColors] = useState<CollectionFilterType["colors"]>([]);
|
||||
const [showHidden, setShowHidden] = useState<boolean>(false);
|
||||
const [compactView] = useSettings("compactView");
|
||||
|
||||
const [active, setActive] = useState<DndItem | null>(null);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, { activationConstraint: { delay: 150, tolerance: 20 } }),
|
||||
useSensor(MouseSensor, { activationConstraint: { delay: 10, tolerance: 20 } }),
|
||||
useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } })
|
||||
);
|
||||
|
||||
const resultList = useMemo(
|
||||
() => sortCollections(filterCollections(collections, { query, colors, showHidden }), sortMode),
|
||||
[query, colors, sortMode, collections, showHidden]
|
||||
() => sortCollections(filterCollections(collections, { query, colors }), sortMode),
|
||||
[query, colors, sortMode, collections]
|
||||
);
|
||||
|
||||
const cls = useStyles_CollectionListView();
|
||||
@@ -51,13 +49,6 @@ export default function CollectionListView(): ReactElement
|
||||
{
|
||||
setQuery("");
|
||||
setColors([]);
|
||||
setShowHidden(false);
|
||||
}, []);
|
||||
|
||||
const updateFilter = useCallback((newColors: CollectionFilterType["colors"], newShowHidden: boolean) =>
|
||||
{
|
||||
setColors(newColors);
|
||||
setShowHidden(newShowHidden);
|
||||
}, []);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent): void =>
|
||||
@@ -96,9 +87,8 @@ export default function CollectionListView(): ReactElement
|
||||
<article className={ cls.root }>
|
||||
<SearchBar
|
||||
query={ query } onQueryChange={ setQuery }
|
||||
filter={ colors } onFilterChange={ updateFilter }
|
||||
filter={ colors } onFilterChange={ setColors }
|
||||
sort={ sortMode } onSortChange={ setSortMode }
|
||||
showHidden={ showHidden }
|
||||
onReset={ resetFilter } />
|
||||
|
||||
<CtaMessage className={ cls.msgBar } />
|
||||
@@ -115,7 +105,7 @@ export default function CollectionListView(): ReactElement
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView, !!(!tilesView && compactView) && cls.compactList) }>
|
||||
<section className={ mergeClasses(cls.collectionList, !tilesView && cls.listView) }>
|
||||
<DndContext
|
||||
sensors={ sensors }
|
||||
collisionDetection={ collisionDetector(!tilesView) }
|
||||
@@ -128,7 +118,7 @@ export default function CollectionListView(): ReactElement
|
||||
strategy={ tilesView ? verticalListSortingStrategy : rectSortingStrategy }
|
||||
>
|
||||
{ resultList.map((collection, index) =>
|
||||
<CollectionView key={ index } collection={ collection } index={ index } compact={ compactView } />
|
||||
<CollectionView key={ index } collection={ collection } index={ index } />
|
||||
) }
|
||||
</SortableContext>
|
||||
|
||||
@@ -145,9 +135,9 @@ export default function CollectionListView(): ReactElement
|
||||
} }
|
||||
>
|
||||
{ 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>
|
||||
:
|
||||
|
||||
@@ -3,48 +3,32 @@ import * as fui from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
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 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 NoColorIcon = ic.bundleIcon(ic.CircleOffFilled, ic.CircleOffRegular);
|
||||
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 (
|
||||
<fui.Menu
|
||||
checkedValues={ values }
|
||||
onCheckedValueChange={ onCheckedValueChange }
|
||||
checkedValues={ !value || value.length < 1 ? { default: ["any"] } : { colors: value } }
|
||||
onCheckedValueChange={ (_, e) =>
|
||||
onChange?.(e.checkedItems.includes("any") ? [] : e.checkedItems as CollectionFilterType["colors"])
|
||||
}
|
||||
>
|
||||
|
||||
<fui.Tooltip relationship="label" content={ i18n.t("main.list.searchbar.filter") }>
|
||||
<fui.MenuTrigger disableButtonEnhancement>
|
||||
<fui.Button appearance="subtle" icon={ <FilterIcon /> } />
|
||||
<fui.Button appearance="subtle" icon={ <ColorFilterIcon /> } />
|
||||
</fui.MenuTrigger>
|
||||
</fui.Tooltip>
|
||||
|
||||
<fui.MenuPopover>
|
||||
<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 /> }>
|
||||
{ i18n.t("colors.any") }
|
||||
</fui.MenuItemCheckbox>
|
||||
@@ -60,11 +44,11 @@ export default function FilterCollectionsButton({ value, onChange, showHidden }:
|
||||
<ColorIcon
|
||||
className={ fui.mergeClasses(
|
||||
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.MenuList>
|
||||
@@ -76,8 +60,7 @@ export default function FilterCollectionsButton({ value, onChange, showHidden }:
|
||||
export type FilterCollectionsButtonProps =
|
||||
{
|
||||
value?: CollectionFilterType["colors"];
|
||||
showHidden?: boolean;
|
||||
onChange?: (value: CollectionFilterType["colors"], showHidden: boolean) => void;
|
||||
onChange?: (value: CollectionFilterType["colors"]) => void;
|
||||
};
|
||||
|
||||
const useStyles = fui.makeStyles({
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function SearchBar(props: SearchBarProps): React.ReactElement
|
||||
<Button appearance="subtle" icon={ <ResetIcon /> } onClick={ props.onReset } />
|
||||
</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 } />
|
||||
</>
|
||||
} />
|
||||
@@ -37,9 +37,8 @@ export type SearchBarProps =
|
||||
query?: string;
|
||||
onQueryChange?: (query: string) => void;
|
||||
filter?: CollectionFilterType["colors"];
|
||||
onFilterChange?: (filter: CollectionFilterType["colors"], showHidden: boolean) => void;
|
||||
onFilterChange?: (filter: CollectionFilterType["colors"]) => void;
|
||||
sort?: CollectionSortMode;
|
||||
showHidden?: boolean;
|
||||
onSortChange?: (sort: CollectionSortMode) => void;
|
||||
onReset?: () => void;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import resolveConflict from "@/features/collectionStorage/utils/resolveConflict";
|
||||
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 exportData from "@/entrypoints/options/utils/exportData";
|
||||
|
||||
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") }
|
||||
</MessageBarBody>
|
||||
<MessageBarActions>
|
||||
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
|
||||
{ i18n.t("options_page.storage.export") }
|
||||
</Button>
|
||||
<Button icon={ <ArrowUpload20Regular /> } onClick={ () => overrideStorageWith("local") }>
|
||||
{ i18n.t("merge_conflict_message.accept_local") }
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||
import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||
|
||||
export default function StorageCapacityIssueMessage(props: MessageBarProps): React.ReactElement
|
||||
export default function StorageCapacityIssueMessage(props: MessageBarProps): JSX.Element
|
||||
{
|
||||
const { usedStorageRatio } = useStorageInfo();
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
|
||||
import { track } from "@/features/analytics";
|
||||
import useSettings, { SettingsValue } from "@/hooks/useSettings";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import { closeTabsAsync } from "@/utils/closeTabsAsync";
|
||||
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
|
||||
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
|
||||
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
|
||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||
import watchTabSelection from "@/utils/watchTabSelection";
|
||||
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
|
||||
import * as ic from "@fluentui/react-icons";
|
||||
@@ -19,26 +14,8 @@ export default function ActionButton(): ReactElement
|
||||
|
||||
const handleAction = async (primary: boolean) =>
|
||||
{
|
||||
const [tabs, skipCount] = await getTabsToSaveAsync();
|
||||
|
||||
if (tabs.length < 1)
|
||||
{
|
||||
await sendPartialSaveNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
const collection: CollectionItem = await createCollectionFromTabs(tabs);
|
||||
await addCollection(collection);
|
||||
|
||||
if (skipCount > 0)
|
||||
await sendPartialSaveNotification();
|
||||
|
||||
const closeTabs: boolean = primary === (defaultAction === "set_aside");
|
||||
|
||||
if (closeTabs)
|
||||
await closeTabsAsync(tabs);
|
||||
|
||||
track(closeTabs ? "set_aside" : "save");
|
||||
const colection = await saveTabsToCollection(primary === (defaultAction === "set_aside"));
|
||||
addCollection(colection);
|
||||
};
|
||||
|
||||
useEffect(() =>
|
||||
|
||||
@@ -11,32 +11,18 @@ import { ReactElement } from "react";
|
||||
export default function MoreButton(): ReactElement
|
||||
{
|
||||
const [tilesView, setTilesView] = useSettings("tilesView");
|
||||
const [compactView, setCompactView] = useSettings("compactView");
|
||||
|
||||
const SettingsIcon: ic.FluentIcon = ic.bundleIcon(ic.Settings20Filled, ic.Settings20Regular);
|
||||
const GridIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular);
|
||||
const CompactIcon: ic.FluentIcon = ic.bundleIcon(ic.ArrowMinimizeVerticalFilled, ic.ArrowMinimizeVerticalRegular);
|
||||
const ViewIcon: ic.FluentIcon = ic.bundleIcon(ic.GridKanban20Filled, ic.GridKanban20Regular);
|
||||
const FeedbackIcon: ic.FluentIcon = ic.bundleIcon(ic.PersonFeedback20Filled, ic.PersonFeedback20Regular);
|
||||
const LearnIcon: ic.FluentIcon = ic.bundleIcon(ic.QuestionCircle20Filled, ic.QuestionCircle20Regular);
|
||||
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 (
|
||||
<fui.Menu
|
||||
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.MenuTrigger disableButtonEnhancement>
|
||||
@@ -50,12 +36,9 @@ export default function MoreButton(): ReactElement
|
||||
<fui.MenuItem icon={ <SettingsIcon /> } onClick={ () => browser.runtime.openOptionsPage() }>
|
||||
{ i18n.t("options_page.title") }
|
||||
</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") }
|
||||
</fui.MenuItemCheckbox>
|
||||
<fui.MenuItemCheckbox name="view" value="compact" icon={ <CompactIcon /> }>
|
||||
{ i18n.t("main.header.menu.compact_view") }
|
||||
</fui.MenuItemCheckbox>
|
||||
|
||||
<fui.MenuDivider />
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import App from "@/App.tsx";
|
||||
import "@/assets/global.css";
|
||||
import { trackPage } from "@/features/analytics";
|
||||
import { useLocalMigration } from "@/features/migration";
|
||||
import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog";
|
||||
import { Divider, makeStyles } from "@fluentui/react-components";
|
||||
@@ -8,8 +7,6 @@ import ReactDOM from "react-dom/client";
|
||||
import CollectionsProvider from "./contexts/CollectionsProvider";
|
||||
import CollectionListView from "./layouts/collections/CollectionListView";
|
||||
import Header from "./layouts/header/Header";
|
||||
import { useSettingsReviewDialog } from "@/features/settingsReview";
|
||||
import useDialogTrain from "@/hooks/useDialogTrain";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<App>
|
||||
@@ -18,17 +15,14 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
);
|
||||
|
||||
document.title = i18n.t("manifest.name");
|
||||
trackPage("collection_list");
|
||||
analytics.page("collection_list");
|
||||
|
||||
function MainPage(): React.ReactElement
|
||||
{
|
||||
const cls = useStyles();
|
||||
|
||||
useLocalMigration();
|
||||
useDialogTrain(
|
||||
useWelcomeDialog,
|
||||
useSettingsReviewDialog
|
||||
);
|
||||
useWelcomeDialog();
|
||||
|
||||
return (
|
||||
<CollectionsProvider>
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
import { track } from "@/features/analytics";
|
||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { Bookmarks } from "wxt/browser";
|
||||
import { getCollectionTitle } from "./getCollectionTitle";
|
||||
|
||||
export default async function exportCollectionToBookmarks(collection: CollectionItem)
|
||||
{
|
||||
const permissions: Browser.permissions.Permissions = await browser.permissions.getAll();
|
||||
|
||||
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({
|
||||
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
||||
title: getCollectionTitle(collection)
|
||||
});
|
||||
|
||||
@@ -41,8 +31,6 @@ export default async function exportCollectionToBookmarks(collection: Collection
|
||||
}
|
||||
}
|
||||
|
||||
track("bookmarks_saved");
|
||||
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.bookmark_saved.title"),
|
||||
message: i18n.t("notifications.bookmark_saved.message"),
|
||||
|
||||
@@ -9,16 +9,13 @@ export default function filterCollections(
|
||||
if (!collections || collections.length < 1)
|
||||
return [];
|
||||
|
||||
if (!filter.query && filter.colors.length < 1 && filter.showHidden)
|
||||
if (!filter.query && filter.colors.length < 1)
|
||||
return collections;
|
||||
|
||||
const query: string = filter.query.toLocaleLowerCase();
|
||||
|
||||
return collections.filter(collection =>
|
||||
{
|
||||
if (filter.showHidden === false && collection.hidden === true)
|
||||
return false;
|
||||
|
||||
let querySatisfied: boolean = query.length < 1 ||
|
||||
getCollectionTitle(collection).toLocaleLowerCase().includes(query);
|
||||
let colorSatisfied: boolean = filter.colors.length < 1 ||
|
||||
@@ -64,6 +61,5 @@ export default function filterCollections(
|
||||
export type CollectionFilterType =
|
||||
{
|
||||
query: string;
|
||||
colors: (`${Browser.tabGroups.Color}` | "none")[];
|
||||
showHidden: boolean;
|
||||
colors: (chrome.tabGroups.ColorEnum | "none")[];
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { TabItem } from "@/models/CollectionModels";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { Tabs } from "wxt/browser";
|
||||
|
||||
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;
|
||||
|
||||
tabs = tabs.filter(i =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
|
||||
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
|
||||
import { settings } from "@/utils/settings";
|
||||
import { Tabs, Windows } from "wxt/browser";
|
||||
|
||||
export async function openCollection(collection: CollectionItem, targetWindow?: "current" | "new" | "incognito"): Promise<void>
|
||||
{
|
||||
@@ -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>
|
||||
{
|
||||
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)
|
||||
));
|
||||
|
||||
@@ -62,21 +63,21 @@ async function createGroup(group: GroupItem, windowId: number, discard?: boolean
|
||||
if (group.pinned === true)
|
||||
return;
|
||||
|
||||
const groupId: number = await browser.tabs.group({
|
||||
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!) as [number, ...number[]],
|
||||
const groupId: number = await chrome.tabs.group({
|
||||
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!),
|
||||
createProperties: { windowId }
|
||||
});
|
||||
|
||||
await browser.tabGroups.update(groupId, {
|
||||
await chrome.tabGroups.update(groupId, {
|
||||
title: group.title,
|
||||
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 ?
|
||||
(await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }))! :
|
||||
const currentWindow: Windows.Window = windowProps ?
|
||||
await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }) :
|
||||
await browser.windows.getCurrent();
|
||||
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!);
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
@@ -101,7 +102,7 @@ async function createTab(url: string, windowId: number, discard: boolean, pinned
|
||||
|
||||
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)
|
||||
return;
|
||||
|
||||
+2
-3
@@ -19,14 +19,13 @@ export default defineConfig([
|
||||
{ files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] },
|
||||
{
|
||||
files: ["**/*.{jsonc,json}"],
|
||||
ignores: [".devcontainer/devcontainer.json", "package-lock.json"],
|
||||
plugins: { json },
|
||||
language: "json/jsonc",
|
||||
extends: ["json/recommended"]
|
||||
},
|
||||
{
|
||||
files: ["**/*.json"],
|
||||
ignores: [".devcontainer/devcontainer.json", "package-lock.json"],
|
||||
ignores: [".devcontainer/devcontainer.json"],
|
||||
plugins: { json },
|
||||
language: "json/json",
|
||||
extends: ["json/recommended"]
|
||||
@@ -51,7 +50,7 @@ export default defineConfig([
|
||||
"@stylistic/semi": ["error", "always"],
|
||||
"@stylistic/block-spacing": ["warn", "always"],
|
||||
"@stylistic/arrow-spacing": ["warn", { before: true, after: true }],
|
||||
"@stylistic/indent": ["warn", "tab", { assignmentOperator: "off" }],
|
||||
"@stylistic/indent": ["warn", "tab"],
|
||||
"@stylistic/quotes": ["error", "double"],
|
||||
"@stylistic/comma-spacing": ["warn"],
|
||||
"@stylistic/comma-dangle": ["warn", "never"],
|
||||
|
||||
@@ -1,55 +1,3 @@
|
||||
import { analytics } from "./utils/analytics";
|
||||
import analyticsPermission from "./utils/analyticsPermission";
|
||||
import { getUserProperties, userId } from "./utils/getUserProperties";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
|
||||
export { default as trackError } from "./utils/trackError";
|
||||
export { default as track } from "./utils/track";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -5,9 +5,6 @@ export { default as getCollections } from "./utils/getCollections";
|
||||
export { default as resoveConflict } from "./utils/resolveConflict";
|
||||
export { default as saveCollections } from "./utils/saveCollections";
|
||||
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 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> =
|
||||
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
|
||||
|
||||
const data: string = decompress(Object.values(chunks).join(""), { inputEncoding: "Base64" });
|
||||
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "Base64" });
|
||||
|
||||
return parseCollections(data);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function parseCollection(data: string): CollectionItem
|
||||
return {
|
||||
type: "collection",
|
||||
timestamp: parseInt(data.match(/(?<=^c)\d+/)!.toString()),
|
||||
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as `${Browser.tabGroups.Color}`,
|
||||
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
|
||||
title: data.match(/(?<=^c[\da-z/]*\|).*/)?.toString(),
|
||||
items: []
|
||||
};
|
||||
@@ -64,7 +64,7 @@ function parseGroup(data: string): GroupItem
|
||||
return {
|
||||
type: "group",
|
||||
pinned: false,
|
||||
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as `${Browser.tabGroups.Color}`,
|
||||
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
|
||||
title: data.match(/(?<=^\tg\/[a-z]+\|).*$/)?.toString(),
|
||||
items: []
|
||||
};
|
||||
@@ -74,7 +74,7 @@ function parseTab(data: string): TabItem
|
||||
{
|
||||
return {
|
||||
type: "tab",
|
||||
url: data.match(/(?<=^\t{1,2}t\|).*(?=\|)/)!.toString(),
|
||||
title: data.match(/(?<=^\t{1,2}t\|.*\|).*$/)?.toString()
|
||||
url: data.match(/(?<=^(\t){1,2}t\|).*(?=\|)/)!.toString(),
|
||||
title: data.match(/(?<=^(\t){1,2}t\|.*\|).*$/)?.toString()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { trackError } from "@/features/analytics";
|
||||
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import saveCollectionsToCloud from "./saveCollectionsToCloud";
|
||||
import saveCollectionsToLocal from "./saveCollectionsToLocal";
|
||||
@@ -17,8 +19,29 @@ export default async function saveCollections(
|
||||
await saveCollectionsToLocal(collections, timestamp);
|
||||
|
||||
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);
|
||||
logger("Save complete");
|
||||
};
|
||||
|
||||
@@ -1,75 +1,46 @@
|
||||
import { trackError } from "@/features/analytics";
|
||||
import { CollectionItem } from "@/models/CollectionModels";
|
||||
import getLogger from "@/utils/getLogger";
|
||||
import sendNotification from "@/utils/sendNotification";
|
||||
import { compress } from "lzutf8";
|
||||
import { WxtStorageItem } from "wxt/storage";
|
||||
import { collectionStorage } from "./collectionStorage";
|
||||
import getChunkKeys from "./getChunkKeys";
|
||||
import serializeCollections from "./serializeCollections";
|
||||
|
||||
const logger = getLogger("saveCollectionsToCloud");
|
||||
|
||||
export default async function saveCollectionsToCloud(collections: CollectionItem[], timestamp: number): Promise<void>
|
||||
{
|
||||
try
|
||||
if (!collections || collections.length < 1)
|
||||
{
|
||||
if (!collections || collections.length < 1)
|
||||
{
|
||||
await browser.storage.sync.set({
|
||||
[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));
|
||||
await collectionStorage.chunkCount.setValue(0);
|
||||
await browser.storage.sync.remove(getChunkKeys());
|
||||
return;
|
||||
}
|
||||
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");
|
||||
console.error(ex);
|
||||
trackError("cloud_save_error", ex as Error);
|
||||
[getStorageKey(collectionStorage.chunkCount)]: chunks.length,
|
||||
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
|
||||
};
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
function splitIntoChunks(data: string): string[]
|
||||
{
|
||||
// QUOTA_BYTES_PER_ITEM includes length of key name, length of content and 2 more bytes (for unknown reason).
|
||||
const chunkKey: string = getChunkKeys(collectionStorage.maxChunkCount - 1)[0];
|
||||
const chunkSize = (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[] = [];
|
||||
|
||||
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 />,
|
||||
"alert",
|
||||
() =>
|
||||
{
|
||||
settingsForReview.removeValue();
|
||||
res();
|
||||
}
|
||||
);
|
||||
else
|
||||
res();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as useSettingsReviewDialog } from "./hooks/useSettingsReviewDialog";
|
||||
@@ -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: []
|
||||
}
|
||||
);
|
||||
@@ -1,25 +1,17 @@
|
||||
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import WelcomeDialog from "../components/WelcomeDialog";
|
||||
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 =>
|
||||
{
|
||||
if (showWelcome || import.meta.env.DEV)
|
||||
dialog.pushCustom(
|
||||
<WelcomeDialog />,
|
||||
"alert",
|
||||
() =>
|
||||
{
|
||||
showWelcomeDialog.removeValue();
|
||||
res();
|
||||
}
|
||||
);
|
||||
else
|
||||
res();
|
||||
dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
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:
|
||||
{
|
||||
"--border": tokens.colorPaletteBlueBorderActive,
|
||||
|
||||
@@ -14,8 +14,8 @@ export default function useStorageInfo(): StorageInfoHook
|
||||
|
||||
return {
|
||||
bytesInUse,
|
||||
storageQuota: browser.storage.sync.QUOTA_BYTES ?? 102400,
|
||||
usedStorageRatio: bytesInUse / (browser.storage.sync.QUOTA_BYTES ?? 102400)
|
||||
storageQuota: chrome.storage.sync.QUOTA_BYTES ?? 102400,
|
||||
usedStorageRatio: bytesInUse / (chrome.storage.sync.QUOTA_BYTES ?? 102400)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,15 +36,6 @@ features:
|
||||
text3: "Visit our dev blog to learn more about this update and all of its features!"
|
||||
actions:
|
||||
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:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Ask for confirmation when deleting an item"
|
||||
show_badge: "Show counter badge"
|
||||
show_notification: "Show notification when saving tabs using context menu"
|
||||
show_partial_save_notification: "Show notification when some tabs couldn't be saved"
|
||||
unload_tabs: "Do not load tabs after opening"
|
||||
allow_analytics: "Allow collection of anonymous statistics"
|
||||
list_locations:
|
||||
title: "Open collection list in:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized."
|
||||
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:
|
||||
title: "About"
|
||||
developed_by: "Developed by Eugene Fox"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "My website"
|
||||
source: "Source code"
|
||||
changelog: "Changelog"
|
||||
privacy: "Privacy policy"
|
||||
|
||||
collections:
|
||||
empty: "This collection is empty"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Add empty group"
|
||||
export_bookmarks: "Export to bookmarks"
|
||||
edit: "Edit collection"
|
||||
hide: "Hide collection"
|
||||
unhide: "Unhide collection"
|
||||
|
||||
groups:
|
||||
title: "Group"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Edit collection"
|
||||
edit_group: "Edit group"
|
||||
edit_tab: "Edit tab"
|
||||
new_group: "New group"
|
||||
new_collection: "New collection"
|
||||
collection_title: "Title"
|
||||
color: "Color"
|
||||
url_error: "URL is required"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Create new collection"
|
||||
menu:
|
||||
tiles_view: "Tiles view"
|
||||
compact_view: "Compact view"
|
||||
changelog: "What's new?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Search"
|
||||
filter: "Filter"
|
||||
show_hidden: "Show hidden"
|
||||
sort:
|
||||
title: "Sort"
|
||||
options:
|
||||
|
||||
@@ -36,15 +36,6 @@ features:
|
||||
text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!"
|
||||
actions:
|
||||
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:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Pedir confirmación al eliminar un elemento"
|
||||
show_badge: "Mostrar insignia de contador"
|
||||
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
|
||||
show_partial_save_notification: "Mostrar notificación cuando algunas pestañas no se pudieron guardar"
|
||||
unload_tabs: "No cargar pestañas después de abrir"
|
||||
allow_analytics: "Permitir la recopilación de estadísticas anónimas"
|
||||
list_locations:
|
||||
title: "Abrir lista de colecciones en:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
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."
|
||||
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:
|
||||
title: "Acerca de"
|
||||
developed_by: "Desarrollado por Eugene Fox"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "Mi sitio web"
|
||||
source: "Código fuente"
|
||||
changelog: "Registro de cambios"
|
||||
privacy: "Política de privacidad"
|
||||
|
||||
collections:
|
||||
empty: "Esta colección está vacía"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Agregar grupo vacío"
|
||||
export_bookmarks: "Exportar a marcadores"
|
||||
edit: "Editar colección"
|
||||
hide: "Ocultar colección"
|
||||
unhide: "Mostrar colección"
|
||||
|
||||
groups:
|
||||
title: "Grupo"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Editar colección"
|
||||
edit_group: "Editar grupo"
|
||||
edit_tab: "Editar pestaña"
|
||||
new_group: "Nuevo grupo"
|
||||
new_collection: "Nueva colección"
|
||||
collection_title: "Título"
|
||||
color: "Color"
|
||||
url_error: "La URL es obligatoria"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Crear nueva colección"
|
||||
menu:
|
||||
tiles_view: "Vista de mosaicos"
|
||||
compact_view: "Vista compacta"
|
||||
changelog: "¿Qué hay de nuevo?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Buscar"
|
||||
filter: "Filtrar"
|
||||
show_hidden: "Mostrar ocultas"
|
||||
sort:
|
||||
title: "Ordenar"
|
||||
options:
|
||||
|
||||
@@ -36,15 +36,6 @@ features:
|
||||
text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!"
|
||||
actions:
|
||||
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:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Chiedi conferma quando elimini un elemento"
|
||||
show_badge: "Mostra il badge del contatore"
|
||||
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
|
||||
show_partial_save_notification: "Mostra notifica quando alcune schede non sono state salvate"
|
||||
unload_tabs: "Non caricare le schede dopo l'apertura"
|
||||
allow_analytics: "Consenti la raccolta di statistiche anonime"
|
||||
list_locations:
|
||||
title: "Apri elenco delle collezioni in:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
disable_prompt:
|
||||
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"
|
||||
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:
|
||||
title: "Informazioni"
|
||||
developed_by: "Sviluppato da Eugene Fox"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "Il mio sito web"
|
||||
source: "Codice sorgente"
|
||||
changelog: "Registro delle modifiche"
|
||||
privacy: "Politica sulla riservatezza"
|
||||
|
||||
collections:
|
||||
empty: "Questa collezione è vuota"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Aggiungi gruppo vuoto"
|
||||
export_bookmarks: "Esporta nei segnalibri"
|
||||
edit: "Modifica collezione"
|
||||
hide: "Nascondi collezione"
|
||||
unhide: "Mostra collezione"
|
||||
|
||||
groups:
|
||||
title: "Gruppo"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Modifica collezione"
|
||||
edit_group: "Modifica gruppo"
|
||||
edit_tab: "Modifica scheda"
|
||||
new_group: "Nuovo gruppo"
|
||||
new_collection: "Nuova collezione"
|
||||
collection_title: "Titolo"
|
||||
color: "Colore"
|
||||
url_error: "L'URL è obbligatorio"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Crea nuova collezione"
|
||||
menu:
|
||||
tiles_view: "Vista a riquadri"
|
||||
compact_view: "Vista compatta"
|
||||
changelog: "Cosa c'è di nuovo?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Cerca"
|
||||
filter: "Filtra"
|
||||
show_hidden: "Mostra nascoste"
|
||||
sort:
|
||||
title: "Ordina"
|
||||
options:
|
||||
|
||||
@@ -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!"
|
||||
actions:
|
||||
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:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Pytaj o potwierdzenie przy usuwaniu elementów"
|
||||
show_badge: "Pokaż licznik"
|
||||
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
|
||||
show_partial_save_notification: "Pokaż powiadomienie, jeśli niektóre karty nie zostały zapisane"
|
||||
unload_tabs: "Nie ładuj kart po otwarciu"
|
||||
allow_analytics: "Zezwól na zbieranie anonimowej statystyki"
|
||||
list_locations:
|
||||
title: "Otwieraj listę kolekcji w:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
disable_prompt:
|
||||
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"
|
||||
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:
|
||||
title: "O rozszerzeniu"
|
||||
developed_by: "Wywoływacz: Eugeniusz Lis"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "Moja strona internetowa"
|
||||
source: "Kod źródłowy"
|
||||
changelog: "Lista zmian"
|
||||
privacy: "Polityka prywatności"
|
||||
|
||||
collections:
|
||||
empty: "Ta kolekcja jest pusta"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Dodaj pustą grupę"
|
||||
export_bookmarks: "Eksportuj do zakładek"
|
||||
edit: "Edytuj kolekcję"
|
||||
hide: "Ukryj kolekcję"
|
||||
unhide: "Pokaż kolekcję"
|
||||
|
||||
groups:
|
||||
title: "Grupa"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Edytuj kolekcję"
|
||||
edit_group: "Edytuj grupę"
|
||||
edit_tab: "Edytuj zakładkę"
|
||||
new_group: "Nowa grupa"
|
||||
new_collection: "Nowa kolekcja"
|
||||
collection_title: "Nazwij"
|
||||
color: "Kolor"
|
||||
url_error: "URL jest wymagany"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Utwórz nową kolekcję"
|
||||
menu:
|
||||
tiles_view: "Kafelki"
|
||||
compact_view: "Widok kompaktowy"
|
||||
changelog: "Co nowego?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Szukaj"
|
||||
filter: "Filtr"
|
||||
show_hidden: "Pokaż ukryte"
|
||||
sort:
|
||||
title: "Sortowanie"
|
||||
options:
|
||||
|
||||
@@ -36,15 +36,6 @@ features:
|
||||
text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!"
|
||||
actions:
|
||||
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:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Pedir confirmação ao excluir um item"
|
||||
show_badge: "Mostrar contador no ícone"
|
||||
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
|
||||
show_partial_save_notification: "Mostrar notificação quando algumas abas não puderam ser salvas"
|
||||
unload_tabs: "Não carregar abas após abrir"
|
||||
allow_analytics: "Permitir coleta de estatísticas anônimas"
|
||||
list_locations:
|
||||
title: "Abrir lista de coleções em:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
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."
|
||||
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:
|
||||
title: "Sobre"
|
||||
developed_by: "Desenvolvido por Eugene Fox"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "Meu site"
|
||||
source: "Código-fonte"
|
||||
changelog: "Registro de alterações"
|
||||
privacy: "Política de Privacidade"
|
||||
|
||||
collections:
|
||||
empty: "Esta coleção está vazia"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Adicionar grupo vazio"
|
||||
export_bookmarks: "Exportar para favoritos"
|
||||
edit: "Editar coleção"
|
||||
hide: "Ocultar coleção"
|
||||
unhide: "Mostrar coleção"
|
||||
|
||||
groups:
|
||||
title: "Grupo"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Editar coleção"
|
||||
edit_group: "Editar grupo"
|
||||
edit_tab: "Editar aba"
|
||||
new_group: "Novo grupo"
|
||||
new_collection: "Nova coleção"
|
||||
collection_title: "Título"
|
||||
color: "Cor"
|
||||
url_error: "A URL é obrigatória"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Criar nova coleção"
|
||||
menu:
|
||||
tiles_view: "Visualização em blocos"
|
||||
compact_view: "Visualização compacta"
|
||||
changelog: "O que há de novo?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Pesquisar"
|
||||
filter: "Filtrar"
|
||||
show_hidden: "Mostrar ocultas"
|
||||
sort:
|
||||
title: "Ordenar"
|
||||
options:
|
||||
|
||||
@@ -36,15 +36,6 @@ features:
|
||||
text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!"
|
||||
actions:
|
||||
visit_blog: "Читать блог"
|
||||
settingsReview:
|
||||
title: "Проверьте настройки"
|
||||
action: "Все настройки"
|
||||
analytics:
|
||||
title: "Эта статистика позволит улучшать расширение"
|
||||
p1: "Мы собираем только статистику использования (количество коллекций, используемые функции и т.д.)"
|
||||
p2: "Мы не собираем ваши личные данные!"
|
||||
p3_text: "Полный список собираемых данных можно посмотреть"
|
||||
p3_link: "здесь"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Спрашивать подтверждение при удалении элементов"
|
||||
show_badge: "Показывать счетчик"
|
||||
show_notification: "Показывать уведомление при сохранении через контекстное меню"
|
||||
show_partial_save_notification: "Показывать уведомление, если некоторые вкладки не были сохранены"
|
||||
unload_tabs: "Не загружать вкладки после открытия"
|
||||
allow_analytics: "Разрешить сбор анонимной статистики"
|
||||
list_locations:
|
||||
title: "Открывать список коллекций в:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке."
|
||||
action: "Отключить и перезагрузить расширение"
|
||||
thumbnail_capture: "Сохранять превью и иконки для сохранённых вкладок"
|
||||
thumbnail_capture_notice1: "Необходим доступ к содержанию посещенных веб-сайтов"
|
||||
thumbnail_capture_notice2: "Отключение этой функции может улучшить производительность при большом количестве сохраненных вкладок"
|
||||
clear_thumbnails:
|
||||
action: "Удалить сохранённые иконки"
|
||||
title: "Удалить превью и иконки?"
|
||||
prompt: "Это действие удалит все превью и иконки у ваших сохраненных вкладок. Это действие не может быть отменено."
|
||||
about:
|
||||
title: "О расширении"
|
||||
developed_by: "Разработчик: Евгений Лис"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "Мой веб-сайт"
|
||||
source: "Исходный код"
|
||||
changelog: "Список изменений"
|
||||
privacy: "Политика конфиденциальности"
|
||||
|
||||
collections:
|
||||
empty: "Эта коллекция пуста"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Добавить пустую группу"
|
||||
export_bookmarks: "Экспортировать в закладки"
|
||||
edit: "Редактировать коллекцию"
|
||||
hide: "Скрыть коллекцию"
|
||||
unhide: "Показать коллекцию"
|
||||
|
||||
groups:
|
||||
title: "Группа"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Редактировать коллекцию"
|
||||
edit_group: "Редактировать группу"
|
||||
edit_tab: "Редактировать вкладку"
|
||||
new_group: "Новая группа"
|
||||
new_collection: "Новая коллекция"
|
||||
collection_title: "Название"
|
||||
color: "Цвет"
|
||||
url_error: "URL является обязательным"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Создать новую коллекцию"
|
||||
menu:
|
||||
tiles_view: "Плитки"
|
||||
compact_view: "Компактный вид"
|
||||
changelog: "Что нового?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Поиск"
|
||||
filter: "Фильтр"
|
||||
show_hidden: "Показать скрытые"
|
||||
sort:
|
||||
title: "Сортировка"
|
||||
options:
|
||||
|
||||
@@ -36,15 +36,6 @@ features:
|
||||
text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!"
|
||||
actions:
|
||||
visit_blog: "Читати блог"
|
||||
settingsReview:
|
||||
title: "Перевірте налаштування"
|
||||
action: "Всi налаштування"
|
||||
analytics:
|
||||
title: "Ця статистика дозволить покращувати розширення"
|
||||
p1: "Ми збираємо лише статистику використання (кількість колекцій, використовувані функції тощо)"
|
||||
p2: "Ми не збираємо ваші особисті дані!"
|
||||
p3_text: "Повний список зібраних даних можна подивитися"
|
||||
p3_link: "тут"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
@@ -82,9 +73,7 @@ options_page:
|
||||
show_delete_prompt: "Запитувати підтвердження при видаленні елементів"
|
||||
show_badge: "Показувати лічильник"
|
||||
show_notification: "Показувати сповіщення при збереженні через контекстне меню"
|
||||
show_partial_save_notification: "Показувати сповіщення, якщо деякі вкладки не були збережені"
|
||||
unload_tabs: "Не завантажувати вкладки після відкриття"
|
||||
allow_analytics: "Дозволити збір анонімної статистики"
|
||||
list_locations:
|
||||
title: "Відкривати список колекцій у:"
|
||||
options:
|
||||
@@ -132,13 +121,6 @@ options_page:
|
||||
disable_prompt:
|
||||
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
|
||||
action: "Вимкнути та перезавантажити розширення"
|
||||
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок"
|
||||
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
|
||||
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
|
||||
clear_thumbnails:
|
||||
action: "Видалити збережені іконки"
|
||||
title: "Видалити превью і іконки?"
|
||||
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
|
||||
about:
|
||||
title: "О розширенні"
|
||||
developed_by: "Розробник: Євген Лис"
|
||||
@@ -151,7 +133,6 @@ options_page:
|
||||
website: "Мій веб-сайт"
|
||||
source: "Вихідний код"
|
||||
changelog: "Список змін"
|
||||
privacy: "Політика конфіденційності"
|
||||
|
||||
collections:
|
||||
empty: "Ця колекція пуста"
|
||||
@@ -184,8 +165,6 @@ collections:
|
||||
add_group: "Додати порожню групу"
|
||||
export_bookmarks: "Експортувати в закладки"
|
||||
edit: "Редагувати колекцію"
|
||||
hide: "Приховати колекцію"
|
||||
unhide: "Показати колекцію"
|
||||
|
||||
groups:
|
||||
title: "Група"
|
||||
@@ -221,25 +200,21 @@ dialogs:
|
||||
title:
|
||||
edit_collection: "Редагувати колекцію"
|
||||
edit_group: "Редагувати групу"
|
||||
edit_tab: "Редагувати вкладку"
|
||||
new_group: "Нова група"
|
||||
new_collection: "Нова колекція"
|
||||
collection_title: "Назва"
|
||||
color: "Колір"
|
||||
url_error: "URL є обов'язковим"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "Створити нову колекцію"
|
||||
menu:
|
||||
tiles_view: "Плитки"
|
||||
compact_view: "Компактний вид"
|
||||
changelog: "Що нового?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "Пошук"
|
||||
filter: "Фільтр"
|
||||
show_hidden: "Показати приховані"
|
||||
sort:
|
||||
title: "Сортування"
|
||||
options:
|
||||
|
||||
+48
-73
@@ -5,8 +5,8 @@ manifest:
|
||||
|
||||
shortcuts:
|
||||
toggle_sidebar: "打开收藏列表"
|
||||
set_aside: "搁置标签页"
|
||||
save_tabs: "保存标签页但不关闭"
|
||||
set_aside: "将标签放到一边"
|
||||
save_tabs: "保存标签而不关闭"
|
||||
|
||||
common:
|
||||
actions:
|
||||
@@ -14,10 +14,10 @@ common:
|
||||
save: "保存"
|
||||
close: "关闭"
|
||||
delete: "删除"
|
||||
reset_filters: "重置筛选"
|
||||
reset_filters: "重置筛选器"
|
||||
cta:
|
||||
feedback: "留下反馈"
|
||||
sponsor: "请我喝杯咖啡!"
|
||||
sponsor: "请我喝咖啡"
|
||||
tooltips:
|
||||
more: "更多"
|
||||
delete_prompt: "您确定吗?此操作无法撤销。"
|
||||
@@ -25,51 +25,42 @@ common:
|
||||
features:
|
||||
v3welcome:
|
||||
title: "欢迎使用搁置的标签页 3.0"
|
||||
text1: "我们很高兴宣布搁置的标签页扩展新的重大更新!"
|
||||
text1: "我们很高兴宣布搁置的标签页扩展的新重大更新!"
|
||||
text2: "此更新带来了全新的用户界面,以及许多新功能,包括:"
|
||||
list:
|
||||
item1: "支持标签组"
|
||||
item2: "收藏自定义"
|
||||
item3: "拖放排序和整理"
|
||||
item4: "从零开始创建收藏"
|
||||
item3: "拖放重新排序和组织"
|
||||
item4: "从头开始手动创建收藏"
|
||||
item5: "以及更多!"
|
||||
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
|
||||
actions:
|
||||
visit_blog: "阅读开发博客"
|
||||
settingsReview:
|
||||
title: "检查您的设置"
|
||||
action: "所有设置"
|
||||
analytics:
|
||||
title: "这些统计数据将帮助我们改进扩展"
|
||||
p1: "我们只收集使用统计数据(收藏数量、使用的功能等)"
|
||||
p2: "我们不会收集您的任何数据!"
|
||||
p3_text: "请参阅我们收集内容的"
|
||||
p3_link: "完整列表"
|
||||
|
||||
notifications:
|
||||
tabs_saved:
|
||||
title: "已创建新收藏"
|
||||
message: "您的标签页已保存到新收藏中"
|
||||
title: "新收藏已创建"
|
||||
message: "您的标签已保存到新收藏中"
|
||||
error_quota_exceeded:
|
||||
title: "超出最大云储存写入操作"
|
||||
message: "我们已将您的标签页保存到本地存储。您需要手动更新云存储"
|
||||
title: "超出最大云写入操作"
|
||||
message: "我们已将您的标签保存到本地存储。您需要手动更新云存储"
|
||||
error_storage_full:
|
||||
title: "您的云存储已满"
|
||||
message: "我们已将您的标签页保存到本地存储。请清理一些云存储空间"
|
||||
message: "我们已将您的标签保存到本地存储。请清理一些云存储空间"
|
||||
bookmark_saved:
|
||||
title: "已导出到书签"
|
||||
message: "您的收藏已导出到书签"
|
||||
partial_save:
|
||||
title: "部分标签页无法保存"
|
||||
message: "部分标签页是无法访问的系统标签页。它们已被跳过"
|
||||
title: "某些标签无法保存"
|
||||
message: "某些标签是我们无法访问的系统标签。它们已被跳过"
|
||||
|
||||
actions:
|
||||
save:
|
||||
all: "保存所有标签页"
|
||||
selected: "保存选定的标签页"
|
||||
all: "保存所有标签"
|
||||
selected: "保存选定的标签"
|
||||
set_aside:
|
||||
all: "搁置所有标签页"
|
||||
selected: "搁置选定的标签页"
|
||||
all: "将所有标签放到一边"
|
||||
selected: "将选定的标签放到一边"
|
||||
show_collections: "显示收藏"
|
||||
|
||||
options_page:
|
||||
@@ -78,13 +69,11 @@ options_page:
|
||||
title: "常规"
|
||||
options:
|
||||
always_show_toolbars: "始终显示工具栏"
|
||||
include_pinned: "保存所有标签页时包括已固定的标签页"
|
||||
include_pinned: "保存所有标签时包括固定标签"
|
||||
show_delete_prompt: "删除项目时要求确认"
|
||||
show_badge: "显示计数角标"
|
||||
show_notification: "使用上下文菜单保存标签页时显示通知"
|
||||
show_partial_save_notification: "如果某些标签页无法保存则显示通知"
|
||||
unload_tabs: "打开后不加载标签页"
|
||||
allow_analytics: "允许收集匿名统计数据"
|
||||
show_badge: "显示计数徽章"
|
||||
show_notification: "使用上下文菜单保存标签时显示通知"
|
||||
unload_tabs: "打开后不加载标签"
|
||||
list_locations:
|
||||
title: "在以下位置打开收藏列表:"
|
||||
options:
|
||||
@@ -103,15 +92,15 @@ options_page:
|
||||
title: "默认操作"
|
||||
options:
|
||||
save_actions:
|
||||
title: "保存标签页时的默认操作"
|
||||
title: "保存标签时的默认操作"
|
||||
options:
|
||||
set_aside: "保存并关闭标签页"
|
||||
save: "保存标签页而不关闭"
|
||||
set_aside: "保存并关闭标签"
|
||||
save: "保存标签而不关闭"
|
||||
restore_actions:
|
||||
title: "打开收藏时的默认操作"
|
||||
options:
|
||||
open: "仅打开标签页"
|
||||
restore: "打开标签页并删除收藏"
|
||||
open: "仅打开标签"
|
||||
restore: "打开标签并删除收藏"
|
||||
storage:
|
||||
title: "存储"
|
||||
capacity:
|
||||
@@ -125,37 +114,29 @@ options_page:
|
||||
import_prompt:
|
||||
title: "导入数据"
|
||||
warning_title: "这是不可逆的操作"
|
||||
warning_text: "这将覆盖您的所有数据!请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
|
||||
warning_text: "这将覆盖您的所有数据。请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
|
||||
proceed: "选择文件"
|
||||
enable: "启用云存储"
|
||||
disable: "禁用云存储"
|
||||
disable_prompt:
|
||||
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
|
||||
action: "禁用并重新加载扩展"
|
||||
thumbnail_capture: "为已保存的标签页保存缩略图和图标"
|
||||
thumbnail_capture_notice1: "需要访问已访问网站内容的权限"
|
||||
thumbnail_capture_notice2: "有大量收藏时,禁用此功能可能会提高性能"
|
||||
clear_thumbnails:
|
||||
action: "删除已保存的图标"
|
||||
title: "删除缩略图和图标?"
|
||||
prompt: "此操作将删除您已保存标签页的所有缩略图和图标。此操作无法撤消。"
|
||||
about:
|
||||
title: "关于"
|
||||
developed_by: "由尤金·福克斯开发"
|
||||
licensed_under: "许可协议"
|
||||
mit_license: "MIT 协议"
|
||||
mit_license: "MIT 许可协议"
|
||||
translation_cta:
|
||||
text: "发现错别字或想为您的语言提供翻译?"
|
||||
button: "快速入门"
|
||||
button: "从这里开始"
|
||||
links:
|
||||
website: "我的网站"
|
||||
source: "源代码"
|
||||
changelog: "更新日志"
|
||||
privacy: "隐私政策"
|
||||
|
||||
collections:
|
||||
empty: "此收藏为空"
|
||||
tabs_count: "$1 个标签页"
|
||||
tabs_count: "$1 个标签"
|
||||
actions:
|
||||
open: "打开所有"
|
||||
restore: "恢复所有"
|
||||
@@ -171,37 +152,35 @@ collections:
|
||||
p1: "扩展需要权限才能在 InPrivate 窗口中打开标签"
|
||||
p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项"
|
||||
firefox:
|
||||
p1: "扩展需要权限才能在隐私窗口中打开标签页"
|
||||
p1: "扩展需要权限才能在隐私窗口中打开标签"
|
||||
p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”"
|
||||
chrome:
|
||||
p1: "扩展需要权限才能在隐身窗口中打开标签页"
|
||||
p1: "扩展需要权限才能在隐身窗口中打开标签"
|
||||
p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项"
|
||||
action: "设置"
|
||||
menu:
|
||||
delete: "删除收藏"
|
||||
add_selected: "添加选定的标签页"
|
||||
add_all: "添加所有标签页"
|
||||
add_group: "添加空分组"
|
||||
add_selected: "添加选定的标签"
|
||||
add_all: "添加所有标签"
|
||||
add_group: "添加空组"
|
||||
export_bookmarks: "导出到书签"
|
||||
edit: "编辑收藏"
|
||||
hide: "隐藏收藏"
|
||||
unhide: "显示收藏"
|
||||
|
||||
groups:
|
||||
title: "分组"
|
||||
title: "组"
|
||||
pinned: "已固定"
|
||||
open: "打开所有"
|
||||
empty: "此分组为空"
|
||||
empty: "此组为空"
|
||||
menu:
|
||||
new_window: "在新窗口中打开"
|
||||
add_selected: "添加选定的标签页"
|
||||
add_all: "添加所有标签页"
|
||||
edit: "编辑分组"
|
||||
add_selected: "添加选定的标签"
|
||||
add_all: "添加所有标签"
|
||||
edit: "编辑组"
|
||||
ungroup: "取消分组"
|
||||
delete: "删除分组"
|
||||
delete: "删除组"
|
||||
|
||||
tabs:
|
||||
delete: "删除标签页"
|
||||
delete: "删除标签"
|
||||
|
||||
colors:
|
||||
none: "无颜色"
|
||||
@@ -220,26 +199,22 @@ dialogs:
|
||||
edit:
|
||||
title:
|
||||
edit_collection: "编辑收藏"
|
||||
edit_group: "编辑分组"
|
||||
edit_tab: "编辑标签页"
|
||||
new_group: "新分组"
|
||||
edit_group: "编辑组"
|
||||
new_group: "新组"
|
||||
new_collection: "新收藏"
|
||||
collection_title: "标题"
|
||||
color: "颜色"
|
||||
url_error: "需要 URL"
|
||||
|
||||
main:
|
||||
header:
|
||||
create_collection: "创建新收藏"
|
||||
menu:
|
||||
tiles_view: "平铺视图"
|
||||
compact_view: "紧凑视图"
|
||||
changelog: "更新内容"
|
||||
changelog: "更新内容?"
|
||||
list:
|
||||
searchbar:
|
||||
title: "搜索"
|
||||
filter: "筛选"
|
||||
show_hidden: "显示隐藏项"
|
||||
sort:
|
||||
title: "排序"
|
||||
options:
|
||||
@@ -250,7 +225,7 @@ main:
|
||||
custom: "自定义"
|
||||
empty:
|
||||
title: "这里还没有内容"
|
||||
message: "搁置当前标签页,或创建新收藏"
|
||||
message: "将当前标签放到一边,或创建新收藏"
|
||||
empty_search:
|
||||
title: "未找到任何内容"
|
||||
message: "尝试更改搜索查询"
|
||||
@@ -272,5 +247,5 @@ parse_error_message:
|
||||
merge_conflict_message:
|
||||
title: "您的本地和云存储有冲突的更改。"
|
||||
message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。"
|
||||
accept_local: "采用本地替换云端"
|
||||
accept_local: "用本地替换"
|
||||
accept_cloud: "接受云端更改"
|
||||
|
||||
@@ -17,7 +17,7 @@ export type DefaultGroupItem =
|
||||
type: "group";
|
||||
pinned?: false;
|
||||
title?: string;
|
||||
color: `${Browser.tabGroups.Color}`;
|
||||
color: chrome.tabGroups.ColorEnum;
|
||||
items: TabItem[];
|
||||
};
|
||||
|
||||
@@ -28,9 +28,8 @@ export type CollectionItem =
|
||||
type: "collection";
|
||||
timestamp: number;
|
||||
title?: string;
|
||||
color?: `${Browser.tabGroups.Color}`;
|
||||
color?: chrome.tabGroups.ColorEnum;
|
||||
items: (TabItem | GroupItem)[];
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type GraphicsStorage = Record<string, GraphicsItem>;
|
||||
|
||||
Generated
-10857
File diff suppressed because it is too large
Load Diff
+27
-34
@@ -1,54 +1,47 @@
|
||||
{
|
||||
"name": "tabs-aside",
|
||||
"private": true,
|
||||
"version": "3.3.2",
|
||||
"version": "3.0.0-rc8",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"build": "npm run lint && wxt build --mv3",
|
||||
"zip": "npm run lint && wxt zip --mv3",
|
||||
"build": "yarn lint && wxt build --mv3",
|
||||
"zip": "yarn lint && wxt zip --mv3",
|
||||
"lint": "tsc --noEmit && eslint . -c eslint.config.js",
|
||||
"prepare": "wxt prepare",
|
||||
"postinstall": "wxt prepare"
|
||||
"postinstall": "yarn prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/modifiers": "^9.0.0",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@fluentui/react-components": "^9.74.1",
|
||||
"@fluentui/react-icons": "^2.0.328",
|
||||
"@webext-core/messaging": "^3.0.1",
|
||||
"@wxt-dev/analytics": "^0.5.4",
|
||||
"@wxt-dev/i18n": "^0.2.5",
|
||||
"@fluentui/react-components": "^9.67.0",
|
||||
"@fluentui/react-icons": "^2.0.306",
|
||||
"@webext-core/messaging": "^2.3.0",
|
||||
"@wxt-dev/analytics": "0.4.1",
|
||||
"@wxt-dev/i18n": "^0.2.4",
|
||||
"lzutf8": "^0.6.3",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7"
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/css": "^1.3.0",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@eslint/json": "^2.0.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/react": "^19.2.16",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@wxt-dev/module-react": "^1.2.2",
|
||||
"eslint": "^9.39.4",
|
||||
"@eslint/css": "^0.10.0",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@eslint/json": "^0.13.0",
|
||||
"@stylistic/eslint-plugin": "^5.2.0",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/scheduler": "^0",
|
||||
"@wxt-dev/module-react": "^1.1.3",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.60.0",
|
||||
"wxt": "^0.20.26"
|
||||
"globals": "^16.3.0",
|
||||
"scheduler": "0.23.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.37.0",
|
||||
"vite": "^6.3.5",
|
||||
"wxt": "~0.19.29"
|
||||
},
|
||||
"overrides": {
|
||||
"node-notifier": {
|
||||
"uuid": "^11.1.1"
|
||||
},
|
||||
"web-ext-run": {
|
||||
"tmp": "^0.2.6"
|
||||
}
|
||||
},
|
||||
"allowScripts": {
|
||||
"esbuild": true,
|
||||
"spawn-sync": true
|
||||
}
|
||||
"packageManager": "yarn@4.9.2"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
**Features**
|
||||
- **Save tabs:** Save all your open tabs in a single click, and restore them later
|
||||
- **Organize tabs:** Create collections and subgroups to organize your saved tabs
|
||||
- **Search tabs:** Quickly find the tabs you need using the search feature
|
||||
- **Sync across devices:** Access your saved tabs from any device with your account
|
||||
- **Go dark:** Dark mode support for a more comfortable browsing experience
|
||||
- **Personalize:** Change the appearance and behavior of the extension to suit your needs
|
||||
<b>Features</b>
|
||||
<ul>
|
||||
<li><b>Save tabs</b>: Save all your open tabs in a single click, and restore them later</li>
|
||||
<li><b>Organize tabs</b>: Create collections and subgroups to organize your saved tabs</li>
|
||||
<li><b>Search tabs</b>: Quickly find the tabs you need using the search feature</li>
|
||||
<li><b>Sync across devices</b>: Access your saved tabs from any device with your account</li>
|
||||
<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!**
|
||||
If you know how to improve this extension you can check [its GitHub Repository](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>Hey, it's an open-source software!</b>
|
||||
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
@@ -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.
|
||||
|
||||
**Características**
|
||||
- **Guardar pestañas:** Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde
|
||||
- **Organizar pestañas:** Crea colecciones y subgrupos para organizar tus pestañas guardadas
|
||||
- **Buscar pestañas:** Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda
|
||||
- **Sincronizar entre dispositivos:** Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta
|
||||
- **Modo oscuro:** Soporte para modo oscuro para una experiencia de navegación más cómoda
|
||||
- **Personalizar:** Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades
|
||||
<b>Características</b>
|
||||
<ul>
|
||||
<li><b>Guardar pestañas</b>: Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde</li>
|
||||
<li><b>Organizar pestañas</b>: Crea colecciones y subgrupos para organizar tus pestañas guardadas</li>
|
||||
<li><b>Buscar pestañas</b>: Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda</li>
|
||||
<li><b>Sincronizar entre dispositivos</b>: Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta</li>
|
||||
<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!**
|
||||
Si sabes cómo mejorar esta extensión, puedes revisar [su repositorio de GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>¡Oye, es un software de código abierto!</b>
|
||||
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
@@ -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.
|
||||
|
||||
**Funzionalità**
|
||||
- **Salva schede:** Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito
|
||||
- **Organizza schede:** Crea collezioni e sottogruppi per organizzare le tue schede salvate
|
||||
- **Cerca schede:** Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca
|
||||
- **Sincronizza tra dispositivi:** Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account
|
||||
- **Modalità scura:** Supporto per la modalità scura per un'esperienza di navigazione più confortevole
|
||||
- **Personalizza:** Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze
|
||||
<b>Funzionalità</b>
|
||||
<ul>
|
||||
<li><b>Salva schede</b>: Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito</li>
|
||||
<li><b>Organizza schede</b>: Crea collezioni e sottogruppi per organizzare le tue schede salvate</li>
|
||||
<li><b>Cerca schede</b>: Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca</li>
|
||||
<li><b>Sincronizza tra dispositivi</b>: Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account</li>
|
||||
<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!**
|
||||
Se sai come migliorare questa estensione, puoi controllare [il suo repository GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>Ehi, è un software open-source!</b>
|
||||
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
@@ -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.
|
||||
|
||||
**Funkcje**
|
||||
- **Zapisywanie kart:** Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później
|
||||
- **Organizacja kart:** Twórz kolekcje i podgrupy, aby organizować zapisane karty
|
||||
- **Wyszukiwanie kart:** Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania
|
||||
- **Synchronizacja między urządzeniami:** Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta
|
||||
- **Tryb ciemny:** Obsługa trybu ciemnego dla bardziej komfortowego użytkowania
|
||||
- **Personalizacja:** Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb
|
||||
<b>Funkcje</b>
|
||||
<ul>
|
||||
<li><b>Zapisywanie kart</b>: Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później</li>
|
||||
<li><b>Organizacja kart</b>: Twórz kolekcje i podgrupy, aby organizować zapisane karty</li>
|
||||
<li><b>Wyszukiwanie kart</b>: Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania</li>
|
||||
<li><b>Synchronizacja między urządzeniami</b>: Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta</li>
|
||||
<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!**
|
||||
Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić [jego repozytorium na GitHubie](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>Przy okazji, to rozszerzenie open-source!</b>
|
||||
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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
**Recursos**
|
||||
- **Salvar abas:** Salve todas as suas abas abertas com um único clique e restaure-as depois
|
||||
- **Organizar abas:** Crie coleções e subgrupos para organizar suas abas salvas
|
||||
- **Pesquisar abas:** Encontre rapidamente as abas que você precisa usando o recurso de pesquisa
|
||||
- **Sincronizar entre dispositivos:** Acesse suas abas salvas de qualquer dispositivo com sua conta
|
||||
- **Modo escuro:** Suporte ao modo escuro para uma experiência de navegação mais confortável
|
||||
- **Personalizar:** Altere a aparência e o comportamento da extensão conforme suas necessidades
|
||||
<b>Recursos</b>
|
||||
<ul>
|
||||
<li><b>Salvar abas</b>: Salve todas as suas abas abertas com um único clique e restaure-as depois</li>
|
||||
<li><b>Organizar abas</b>: Crie coleções e subgrupos para organizar suas abas salvas</li>
|
||||
<li><b>Pesquisar abas</b>: Encontre rapidamente as abas que você precisa usando o recurso de pesquisa</li>
|
||||
<li><b>Sincronizar entre dispositivos</b>: Acesse suas abas salvas de qualquer dispositivo com sua conta</li>
|
||||
<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!**
|
||||
Se você sabe como melhorar esta extensão, confira [seu repositório no GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>Ei, é um software de código aberto!</b>
|
||||
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
@@ -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 по ссылке
|
||||
|
||||
**Кстати это опенсорс расширение!**
|
||||
Если вы знаете, как можно его улучшить, можете перейти на [страницу GitHub репозитория проекта](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>Кстати это опенсорс расширение!</b>
|
||||
Если вы знаете, как можно его улучшить, можете перейти на <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
@@ -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
|
||||
|
||||
**До речі, це опенсорс розширення!**
|
||||
Якщо ви знаєте, як покращити це розширення, ви можете відвідати [його репозиторій на GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>До речі, це опенсорс розширення!</b>
|
||||
Якщо ви знаєте, як покращити це розширення, ви можете відвідати <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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
**嘿,这是一个开源软件!**
|
||||
如果您知道如何改进此扩展,可以查看[其 GitHub 仓库](https://github.com/xfox111/TabsAsideExtension)
|
||||
<b>嘿,这是一个开源软件!</b>
|
||||
如果您知道如何改进此扩展,可以查看<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
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx",
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
+1
-4
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
type ProtocolMap =
|
||||
@@ -7,9 +7,6 @@ type ProtocolMap =
|
||||
addThumbnail(data: { url: string; thumbnail: string; }): void;
|
||||
getGraphicsCache(): GraphicsStorage;
|
||||
refreshCollections(): void;
|
||||
|
||||
openCollection(data: { collection: CollectionItem; targetWindow: "new" | "incognito"; }): void;
|
||||
openGroup(data: { group: GroupItem; newWindow: boolean; }): void;
|
||||
};
|
||||
|
||||
function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { track } from "@/features/analytics";
|
||||
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
|
||||
import { Tabs } from "wxt/browser";
|
||||
import sendNotification from "./sendNotification";
|
||||
import { settings } from "./settings";
|
||||
|
||||
export default async function saveTabsToCollection(closeTabs: boolean): Promise<CollectionItem>
|
||||
{
|
||||
let tabs: Tabs.Tab[] = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
highlighted: true
|
||||
});
|
||||
|
||||
if (tabs.length < 2)
|
||||
{
|
||||
const ignorePinned: boolean = await settings.ignorePinned.getValue();
|
||||
tabs = await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
pinned: ignorePinned ? false : undefined
|
||||
});
|
||||
}
|
||||
|
||||
const [collection, tabsToClose] = await createCollectionFromTabs(tabs);
|
||||
|
||||
if (closeTabs)
|
||||
{
|
||||
await browser.tabs.create({
|
||||
active: true,
|
||||
windowId: tabs[0].windowId
|
||||
});
|
||||
await browser.tabs.remove(tabsToClose.map(i => i.id!));
|
||||
}
|
||||
|
||||
track(closeTabs ? "set_aside" : "save");
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionItem, Tabs.Tab[]]>
|
||||
{
|
||||
if (tabs.length < 1)
|
||||
return [{ type: "collection", timestamp: Date.now(), items: [] }, []];
|
||||
|
||||
const tabCount: number = tabs.length;
|
||||
|
||||
tabs = tabs.filter(i =>
|
||||
i.url
|
||||
&& new URL(i.url).protocol !== "about:"
|
||||
&& new URL(i.url).hostname !== "newtab"
|
||||
);
|
||||
|
||||
if (tabs.length < tabCount)
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.partial_save.title"),
|
||||
message: i18n.t("notifications.partial_save.message"),
|
||||
icon: "/notification_icons/save_warning.png"
|
||||
});
|
||||
|
||||
tabs = tabs.filter(i => !i.url!.startsWith(browser.runtime.getURL("/")));
|
||||
|
||||
const collection: CollectionItem = {
|
||||
type: "collection",
|
||||
timestamp: Date.now(),
|
||||
items: []
|
||||
};
|
||||
|
||||
let tabIndex: number = 0;
|
||||
|
||||
if (tabs[tabIndex].pinned)
|
||||
{
|
||||
collection.items.push({ type: "group", pinned: true, items: [] });
|
||||
|
||||
for (; tabIndex < tabs.length; tabIndex++)
|
||||
{
|
||||
if (!tabs[tabIndex].pinned)
|
||||
break;
|
||||
|
||||
(collection.items[0] as GroupItem).items.push({
|
||||
type: "tab",
|
||||
url: tabs[tabIndex].url!,
|
||||
title: tabs[tabIndex].title
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Special case, if all tabs are in the same group, create a collection with the group title
|
||||
if (tabs[0].groupId && tabs[0].groupId !== -1 &&
|
||||
tabs.every(i => i.groupId === tabs[0].groupId)
|
||||
)
|
||||
{
|
||||
const group = await chrome.tabGroups.get(tabs[0].groupId);
|
||||
collection.title = group.title;
|
||||
collection.color = group.color;
|
||||
|
||||
tabs.forEach(i =>
|
||||
collection.items.push({ type: "tab", url: i.url!, title: i.title })
|
||||
);
|
||||
|
||||
return [collection, tabs];
|
||||
}
|
||||
|
||||
let activeGroup: number | null = null;
|
||||
|
||||
for (; tabIndex < tabs.length; tabIndex++)
|
||||
{
|
||||
const tab = tabs[tabIndex];
|
||||
|
||||
if (!tab.groupId || tab.groupId === -1)
|
||||
{
|
||||
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
|
||||
activeGroup = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!activeGroup || activeGroup !== tab.groupId)
|
||||
{
|
||||
activeGroup = tab.groupId;
|
||||
const group = await chrome.tabGroups.get(activeGroup);
|
||||
|
||||
collection.items.push({
|
||||
type: "group",
|
||||
color: group.color,
|
||||
title: group.title,
|
||||
items: []
|
||||
});
|
||||
}
|
||||
|
||||
(collection.items[collection.items.length - 1] as GroupItem).items.push({
|
||||
type: "tab",
|
||||
url: tab.url!,
|
||||
title: tab.title
|
||||
});
|
||||
}
|
||||
|
||||
return [collection, tabs];
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import sendNotification from "./sendNotification";
|
||||
import { settings } from "./settings";
|
||||
|
||||
export default async function sendPartialSaveNotification(): Promise<void>
|
||||
{
|
||||
if (await settings.showPartialSaveNotification.getValue())
|
||||
await sendNotification({
|
||||
title: i18n.t("notifications.partial_save.title"),
|
||||
message: i18n.t("notifications.partial_save.message"),
|
||||
icon: "/notification_icons/save_warning.png"
|
||||
});
|
||||
}
|
||||
@@ -95,21 +95,5 @@ export const settings = {
|
||||
fallback: true,
|
||||
version: 1
|
||||
}
|
||||
),
|
||||
|
||||
showPartialSaveNotification: storage.defineItem<boolean>(
|
||||
"sync:showPartialSaveNotification",
|
||||
{
|
||||
fallback: true,
|
||||
version: 1
|
||||
}
|
||||
),
|
||||
|
||||
compactView: storage.defineItem<boolean>(
|
||||
"sync:compactView",
|
||||
{
|
||||
fallback: false,
|
||||
version: 1
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user