mirror of
https://github.com/XFox111/TabsAsideExtension.git
synced 2026-07-02 19:52:47 +03:00
Compare commits
7 Commits
v3.0.0-rc6
..
v3.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
| c27c9f7b33 | |||
| def52278ff | |||
| 101a72e6e3 | |||
| e21022d985 | |||
| 735089eb59 | |||
| 29a9d57348 | |||
| 2bd9337e63 |
@@ -38,7 +38,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:20
|
container: node:24
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -51,7 +51,19 @@ jobs:
|
|||||||
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
|
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
|
||||||
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
|
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
- run: yarn install
|
- 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: yarn zip -b ${{ matrix.target }}
|
- run: yarn zip -b ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Drop build artifacts (${{ matrix.target }})
|
- name: Drop build artifacts (${{ matrix.target }})
|
||||||
@@ -63,12 +75,13 @@ jobs:
|
|||||||
|
|
||||||
- name: web-ext lint
|
- name: web-ext lint
|
||||||
if: ${{ matrix.target == 'firefox' }}
|
if: ${{ matrix.target == 'firefox' }}
|
||||||
uses: freaktechnik/web-ext-lint@main
|
uses: kewisch/action-web-ext@main
|
||||||
with:
|
with:
|
||||||
extension-root: ./.output/firefox-mv3
|
cmd: lint
|
||||||
self-hosted: false
|
source: ./.output/firefox-mv3
|
||||||
|
channel: listed
|
||||||
|
|
||||||
- run: yarn audit
|
- run: yarn npm audit
|
||||||
continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }}
|
continue-on-error: ${{ github.event_name != 'release' && github.event.inputs.bypass_audit == 'true' }}
|
||||||
|
|
||||||
publish-github:
|
publish-github:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@main
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
extver=`jq -r ".version" package.json`
|
extver=`jq -r ".version" package.json`
|
||||||
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
echo "version=$extver" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- uses: dev-build-deploy/release-me@v0.18.0
|
- uses: dev-build-deploy/release-me@v0.18.2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
prefix: v
|
prefix: v
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node:23
|
container: node:24
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -43,7 +43,19 @@ jobs:
|
|||||||
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
|
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
|
||||||
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
|
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
|
||||||
|
|
||||||
|
- run: corepack enable
|
||||||
- run: yarn install
|
- 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: yarn zip -b ${{ matrix.target }}
|
- run: yarn zip -b ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Drop artifacts (${{ matrix.target }})
|
- name: Drop artifacts (${{ matrix.target }})
|
||||||
@@ -55,9 +67,10 @@ jobs:
|
|||||||
|
|
||||||
- name: web-ext lint
|
- name: web-ext lint
|
||||||
if: ${{ matrix.target == 'firefox' }}
|
if: ${{ matrix.target == 'firefox' }}
|
||||||
uses: freaktechnik/web-ext-lint@main
|
uses: kewisch/action-web-ext@main
|
||||||
with:
|
with:
|
||||||
extension-root: ./.output/firefox-mv3
|
cmd: lint
|
||||||
self-hosted: false
|
source: ./.output/firefox-mv3
|
||||||
|
channel: listed
|
||||||
|
|
||||||
- run: yarn audit
|
- run: yarn npm audit
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pnpm-debug.log*
|
|||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
.yarn/
|
||||||
.output
|
.output
|
||||||
stats.html
|
stats.html
|
||||||
stats-*.json
|
stats-*.json
|
||||||
|
|||||||
+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"
|
||||||
+35
-2
@@ -6,7 +6,7 @@
|
|||||||
- Thumbnails of saved tabs
|
- Thumbnails of saved tabs
|
||||||
3. This extension uses Google Analytics to collect usage statistics and improve the extension.
|
3. This extension uses Google Analytics to collect usage statistics and improve the extension.
|
||||||
4. This extension uses analytics to collect following data:
|
4. This extension uses analytics to collect following data:
|
||||||
- Random UUID to identify the user
|
- Random UUID to distinguish unique users
|
||||||
- Browser name and version
|
- Browser name and version
|
||||||
- Operating system name and version
|
- Operating system name and version
|
||||||
- System architecture
|
- System architecture
|
||||||
@@ -14,7 +14,40 @@
|
|||||||
- Extension language
|
- Extension language
|
||||||
- User settings
|
- User settings
|
||||||
- Number of saved collections
|
- Number of saved collections
|
||||||
- Action identifiers (e.g. "page_view", "extension_installed", "item_created", etc.)
|
- 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
|
||||||
4. This extension does not collect or use any personally identifiable information (PII) or sensitive data for purposes other than its core functionality.
|
4. This extension does not collect or use any personally identifiable information (PII) or sensitive data for purposes other than its core functionality.
|
||||||
5. This extension uses cloud storage built into your browser to store its data.
|
5. This extension uses cloud storage built into your browser to store its data.
|
||||||
6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
|
6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ Check out our [latest blog post](https://at.xfox111.net/tabs-aside-3-0) regardin
|
|||||||
|
|
||||||
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin)
|
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin)
|
||||||
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd)
|
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd)
|
||||||
- [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/)
|
- [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/ms-edge-tabs-aside/)
|
||||||
- [GitHub Releases](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
- [GitHub Releases](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|
||||||
### Sideloading (for testing purposes only)
|
### Sideloading (for testing purposes only)
|
||||||
@@ -81,7 +81,7 @@ If you want to sideload it without replacing to run both versions at the same ti
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
[](https://github.com/xfox111/TabsAsideExtension/issues)
|
[](https://github.com/xfox111/TabsAsideExtension/issues)
|
||||||
[](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yaml)
|
[](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yml)
|
||||||
[](https://github.com/xfox111/TabsAsideExtension)
|
[](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
There are many ways in which you can participate in the project, for example:
|
There are many ways in which you can participate in the project, for example:
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -39,16 +39,19 @@ body
|
|||||||
/* Handle */
|
/* Handle */
|
||||||
::-webkit-scrollbar-thumb
|
::-webkit-scrollbar-thumb
|
||||||
{
|
{
|
||||||
|
/* eslint-disable-next-line css/no-invalid-properties */
|
||||||
background-color: var(--colorNeutralStroke1);
|
background-color: var(--colorNeutralStroke1);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover
|
::-webkit-scrollbar-thumb:hover
|
||||||
{
|
{
|
||||||
|
/* eslint-disable-next-line css/no-invalid-properties */
|
||||||
background-color: var(--colorNeutralStroke1Hover);
|
background-color: var(--colorNeutralStroke1Hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover:active
|
::-webkit-scrollbar-thumb:hover:active
|
||||||
{
|
{
|
||||||
|
/* eslint-disable-next-line css/no-invalid-properties */
|
||||||
background-color: var(--colorNeutralStroke1Pressed);
|
background-color: var(--colorNeutralStroke1Pressed);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,8 @@ export const githubLinks =
|
|||||||
repo: githubLink(),
|
repo: githubLink(),
|
||||||
release: githubLink(`releases/tag/v${Package.version}`),
|
release: githubLink(`releases/tag/v${Package.version}`),
|
||||||
license: githubLink("blob/main/LICENSE"),
|
license: githubLink("blob/main/LICENSE"),
|
||||||
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations")
|
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations"),
|
||||||
|
privacy: githubLink("blob/main/PRIVACY.md")
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storeLink: string =
|
export const storeLink: string =
|
||||||
|
|||||||
+82
-26
@@ -1,5 +1,5 @@
|
|||||||
import { track, trackError } from "@/features/analytics";
|
import { track, trackError } from "@/features/analytics";
|
||||||
import { collectionCount, getCollections, saveCollections } from "@/features/collectionStorage";
|
import { collectionCount, getCollections, thumbnailCaptureEnabled, saveCollections } from "@/features/collectionStorage";
|
||||||
import { migrateStorage } from "@/features/migration";
|
import { migrateStorage } from "@/features/migration";
|
||||||
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
|
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
|
||||||
import { SettingsValue } from "@/hooks/useSettings";
|
import { SettingsValue } from "@/hooks/useSettings";
|
||||||
@@ -12,13 +12,16 @@ import { settings } from "@/utils/settings";
|
|||||||
import watchTabSelection from "@/utils/watchTabSelection";
|
import watchTabSelection from "@/utils/watchTabSelection";
|
||||||
import { Tabs, Windows } from "wxt/browser";
|
import { Tabs, Windows } from "wxt/browser";
|
||||||
import { Unwatch } from "wxt/storage";
|
import { Unwatch } from "wxt/storage";
|
||||||
|
import { openCollection, openGroup } from "./sidepanel/utils/opener";
|
||||||
|
import { setSettingsReviewNeeded } from "@/features/settingsReview/utils";
|
||||||
|
import { RemoveListenerCallback } from "@webext-core/messaging";
|
||||||
|
|
||||||
export default defineBackground(() =>
|
export default defineBackground(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const logger = getLogger("background");
|
const logger = getLogger("background");
|
||||||
const graphicsCache: GraphicsStorage = {};
|
let graphicsCache: GraphicsStorage = {};
|
||||||
let listLocation: SettingsValue<"listLocation"> = "sidebar";
|
let listLocation: SettingsValue<"listLocation"> = "sidebar";
|
||||||
|
|
||||||
logger("Background script started");
|
logger("Background script started");
|
||||||
@@ -35,6 +38,8 @@ export default defineBackground(() =>
|
|||||||
|
|
||||||
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
|
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
|
||||||
|
|
||||||
|
await setSettingsReviewNeeded(reason, previousVersion);
|
||||||
|
|
||||||
if (reason === "update" && previousMajor < 3)
|
if (reason === "update" && previousMajor < 3)
|
||||||
{
|
{
|
||||||
await migrateStorage();
|
await migrateStorage();
|
||||||
@@ -43,36 +48,37 @@ export default defineBackground(() =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.tabs.onUpdated.addListener((_, __, tab) =>
|
|
||||||
{
|
|
||||||
if (!tab.url)
|
|
||||||
return;
|
|
||||||
|
|
||||||
graphicsCache[tab.url] = {
|
|
||||||
preview: graphicsCache[tab.url]?.preview,
|
|
||||||
capture: graphicsCache[tab.url]?.capture,
|
|
||||||
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
browser.commands.onCommand.addListener(
|
browser.commands.onCommand.addListener(
|
||||||
(command, tab) => performContextAction(command, tab!.windowId!)
|
(command, tab) => performContextAction(command, tab!.windowId!)
|
||||||
);
|
);
|
||||||
|
|
||||||
onMessage("getGraphicsCache", () => graphicsCache);
|
onMessage("getGraphicsCache", () => graphicsCache);
|
||||||
onMessage("addThumbnail", ({ data }) =>
|
onMessage("refreshCollections", () => { });
|
||||||
|
|
||||||
|
if (import.meta.env.FIREFOX)
|
||||||
{
|
{
|
||||||
graphicsCache[data.url] = {
|
onMessage("openCollection", ({ data }) => openCollection(data.collection, data.targetWindow));
|
||||||
preview: data.thumbnail,
|
onMessage("openGroup", ({ data }) => openGroup(data.group, data.newWindow));
|
||||||
capture: graphicsCache[data.url]?.capture,
|
}
|
||||||
icon: graphicsCache[data.url]?.icon
|
|
||||||
};
|
|
||||||
});
|
|
||||||
onMessage("refreshCollections", () => {});
|
|
||||||
|
|
||||||
setupTabCaputre();
|
setupTabCaputre();
|
||||||
async function setupTabCaputre(): Promise<void>
|
async function setupTabCaputre(): Promise<void>
|
||||||
{
|
{
|
||||||
|
let unwatchAddThumbnail: RemoveListenerCallback | null = null;
|
||||||
|
let captureInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
const captureFavicon = (_: any, __: any, tab: 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: Tabs.Tab): Promise<void> =>
|
const tryCaptureTab = async (tab: Tabs.Tab): Promise<void> =>
|
||||||
{
|
{
|
||||||
if (!tab.url || tab.status !== "complete" || !tab.active)
|
if (!tab.url || tab.status !== "complete" || !tab.active)
|
||||||
@@ -106,11 +112,61 @@ export default defineBackground(() =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setInterval(() =>
|
const updateCapture = async (captureThumbnails: boolean): Promise<void> =>
|
||||||
{
|
{
|
||||||
browser.tabs.query({ active: true })
|
const scriptingGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"] });
|
||||||
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
|
|
||||||
}, 1000);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
setupContextMenu();
|
setupContextMenu();
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import { sendMessage } from "@/utils/messaging";
|
|||||||
// This content script is injected into each browser tab.
|
// This content script is injected into each browser tab.
|
||||||
// It's purpose is to retrive an OpenGraph thumbnail URL from the metadata
|
// It's purpose is to retrive an OpenGraph thumbnail URL from the metadata
|
||||||
|
|
||||||
export default defineContentScript({
|
export default defineUnlistedScript({ main });
|
||||||
matches: ["<all_urls>"],
|
|
||||||
runAt: "document_idle",
|
|
||||||
main
|
|
||||||
});
|
|
||||||
|
|
||||||
const logger = getLogger("contentScript");
|
const logger = getLogger("contentScript");
|
||||||
|
|
||||||
@@ -34,5 +34,12 @@ export const useOptionsStyles = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: tokens.spacingHorizontalS
|
gap: tokens.spacingHorizontalS
|
||||||
|
},
|
||||||
|
group:
|
||||||
|
{
|
||||||
|
display: "flex",
|
||||||
|
flexFlow: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: tokens.spacingVerticalSNudge
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ export default function AboutSection(): React.ReactElement
|
|||||||
<Body1 as="p">
|
<Body1 as="p">
|
||||||
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
|
<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.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
|
||||||
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link>
|
<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>
|
||||||
|
|
||||||
<div className={ cls.horizontalButtons }>
|
<div className={ cls.horizontalButtons }>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import useSettings, { SettingsValue } from "@/hooks/useSettings";
|
|||||||
import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
|
import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
|
||||||
import { KeyCommand20Regular } from "@fluentui/react-icons";
|
import { KeyCommand20Regular } from "@fluentui/react-icons";
|
||||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||||
|
import { analyticsPermission } from "@/features/analytics";
|
||||||
|
|
||||||
export default function GeneralSection(): React.ReactElement
|
export default function GeneralSection(): React.ReactElement
|
||||||
{
|
{
|
||||||
@@ -14,8 +15,23 @@ export default function GeneralSection(): React.ReactElement
|
|||||||
const [listLocation, setListLocation] = useSettings("listLocation");
|
const [listLocation, setListLocation] = useSettings("listLocation");
|
||||||
const [contextAction, setContextAction] = useSettings("contextAction");
|
const [contextAction, setContextAction] = useSettings("contextAction");
|
||||||
|
|
||||||
|
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const cls = useOptionsStyles();
|
const cls = useOptionsStyles();
|
||||||
|
|
||||||
|
useEffect(() =>
|
||||||
|
{
|
||||||
|
analyticsPermission.getValue().then(setAllowAnalytics);
|
||||||
|
return analyticsPermission.watch(setAllowAnalytics);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateAnalytics = (enabled: boolean): void =>
|
||||||
|
{
|
||||||
|
setAllowAnalytics(null);
|
||||||
|
analyticsPermission.setValue(enabled)
|
||||||
|
.catch(() => setAllowAnalytics(!enabled));
|
||||||
|
};
|
||||||
|
|
||||||
const openShortcutsPage = (): Promise<any> =>
|
const openShortcutsPage = (): Promise<any> =>
|
||||||
browser.tabs.create({
|
browser.tabs.create({
|
||||||
url: "chrome://extensions/shortcuts",
|
url: "chrome://extensions/shortcuts",
|
||||||
@@ -60,6 +76,12 @@ export default function GeneralSection(): React.ReactElement
|
|||||||
label={ i18n.t("options_page.general.options.unload_tabs") }
|
label={ i18n.t("options_page.general.options.unload_tabs") }
|
||||||
checked={ dismissOnLoad ?? false }
|
checked={ dismissOnLoad ?? false }
|
||||||
onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } />
|
onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } />
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label={ i18n.t("options_page.general.options.allow_analytics") }
|
||||||
|
checked={ allowAnalytics ?? true }
|
||||||
|
disabled={ allowAnalytics === null }
|
||||||
|
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Field label={ i18n.t("options_page.general.options.list_locations.title") }>
|
<Field label={ i18n.t("options_page.general.options.list_locations.title") }>
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { useDialog } from "@/contexts/DialogProvider";
|
import { useDialog } from "@/contexts/DialogProvider";
|
||||||
import { cloudDisabled, setCloudStorage } from "@/features/collectionStorage";
|
import { clearGraphicsStorage, cloudDisabled, setCloudStorage, thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||||
import { Button, Field, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar } from "@fluentui/react-components";
|
import { Button, Field, InfoLabel, LabelProps, MessageBar, MessageBarBody, MessageBarTitle, ProgressBar, Switch } from "@fluentui/react-components";
|
||||||
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
|
import { ArrowDownload20Regular, ArrowUpload20Regular } from "@fluentui/react-icons";
|
||||||
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
import { useOptionsStyles } from "../hooks/useOptionsStyles";
|
||||||
import exportData from "../utils/exportData";
|
import exportData from "../utils/exportData";
|
||||||
import importData from "../utils/importData";
|
import importData from "../utils/importData";
|
||||||
|
import { Unwatch } from "wxt/storage";
|
||||||
|
|
||||||
export default function StorageSection(): React.ReactElement
|
export default function StorageSection(): React.ReactElement
|
||||||
{
|
{
|
||||||
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
|
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
|
||||||
const [importResult, setImportResult] = useState<boolean | null>(null);
|
const [importResult, setImportResult] = useState<boolean | null>(null);
|
||||||
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
|
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
|
||||||
|
const [isThumbnailCaptureEnabled, setThumbnailCaptureEnabled] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const cls = useOptionsStyles();
|
const cls = useOptionsStyles();
|
||||||
@@ -20,10 +22,35 @@ export default function StorageSection(): React.ReactElement
|
|||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
|
thumbnailCaptureEnabled.getValue().then(setThumbnailCaptureEnabled);
|
||||||
cloudDisabled.getValue().then(setCloudDisabled);
|
cloudDisabled.getValue().then(setCloudDisabled);
|
||||||
return cloudDisabled.watch(setCloudDisabled);
|
|
||||||
|
const unwatchCloud: Unwatch = cloudDisabled.watch(setCloudDisabled);
|
||||||
|
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setThumbnailCaptureEnabled);
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
{
|
||||||
|
unwatchCloud();
|
||||||
|
unwatchThumbnails();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleSetThumbnailCapture = (enabled: boolean): void =>
|
||||||
|
{
|
||||||
|
setThumbnailCaptureEnabled(null);
|
||||||
|
thumbnailCaptureEnabled.setValue(enabled)
|
||||||
|
.catch(() => setThumbnailCaptureEnabled(!enabled));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearThumbnails = (): void =>
|
||||||
|
dialog.pushPrompt({
|
||||||
|
title: i18n.t("options_page.storage.clear_thumbnails.title"),
|
||||||
|
content: i18n.t("options_page.storage.clear_thumbnails.prompt"),
|
||||||
|
confirmText: i18n.t("common.actions.delete"),
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: () => clearGraphicsStorage()
|
||||||
|
});
|
||||||
|
|
||||||
const handleImport = (): void =>
|
const handleImport = (): void =>
|
||||||
dialog.pushPrompt({
|
dialog.pushPrompt({
|
||||||
title: i18n.t("options_page.storage.import_prompt.title"),
|
title: i18n.t("options_page.storage.import_prompt.title"),
|
||||||
@@ -51,6 +78,29 @@ export default function StorageSection(): React.ReactElement
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div className={ cls.group }>
|
||||||
|
<Switch
|
||||||
|
checked={ isThumbnailCaptureEnabled ?? true }
|
||||||
|
disabled={ isThumbnailCaptureEnabled === null }
|
||||||
|
onChange={ (_, e) => handleSetThumbnailCapture(e.checked as boolean) }
|
||||||
|
label={ {
|
||||||
|
children: (_: any, props: LabelProps) =>
|
||||||
|
<InfoLabel
|
||||||
|
{ ...props }
|
||||||
|
label={ i18n.t("options_page.storage.thumbnail_capture") }
|
||||||
|
info={
|
||||||
|
<p>
|
||||||
|
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }<br /><br />
|
||||||
|
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
|
||||||
|
</p>
|
||||||
|
} />
|
||||||
|
} } />
|
||||||
|
|
||||||
|
<Button onClick={ handleClearThumbnails } className={ dangerCls.buttonSubtle } appearance="subtle">
|
||||||
|
{ i18n.t("options_page.storage.clear_thumbnails.action") }
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{ isCloudDisabled === false &&
|
{ isCloudDisabled === false &&
|
||||||
<Field
|
<Field
|
||||||
label={ i18n.t("options_page.storage.capacity.title") }
|
label={ i18n.t("options_page.storage.capacity.title") }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import App from "@/App.tsx";
|
import App from "@/App.tsx";
|
||||||
import "@/assets/global.css";
|
import "@/assets/global.css";
|
||||||
|
import { trackPage } from "@/features/analytics";
|
||||||
import { Tab, TabList } from "@fluentui/react-components";
|
import { Tab, TabList } from "@fluentui/react-components";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
|
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
|
||||||
@@ -14,7 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
</App>
|
</App>
|
||||||
);
|
);
|
||||||
|
|
||||||
analytics.page("options_page");
|
trackPage("options_page");
|
||||||
|
|
||||||
function OptionsPage(): React.ReactElement
|
function OptionsPage(): React.ReactElement
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ export default async function importData(): Promise<boolean | null>
|
|||||||
await browser.storage.local.set(data.local);
|
await browser.storage.local.set(data.local);
|
||||||
|
|
||||||
if (data.sync)
|
if (data.sync)
|
||||||
|
{
|
||||||
|
if (import.meta.env.FIREFOX && data.sync.contextAction === "context")
|
||||||
|
data.sync.contextAction = "open";
|
||||||
|
|
||||||
await browser.storage.sync.set(data.sync);
|
await browser.storage.sync.set(data.sync);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error)
|
catch (error)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ export const useStyles_CollectionView = makeStyles({
|
|||||||
{
|
{
|
||||||
gridAutoFlow: "row",
|
gridAutoFlow: "row",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
paddingBottom: tokens.spacingVerticalS
|
paddingBottom: tokens.spacingVerticalS,
|
||||||
|
gridAutoRows: import.meta.env.FIREFOX ? "min-content" : undefined
|
||||||
},
|
},
|
||||||
dragOverlay:
|
dragOverlay:
|
||||||
{
|
{
|
||||||
@@ -84,15 +85,11 @@ export const useStyles_CollectionView = makeStyles({
|
|||||||
"& > div":
|
"& > div":
|
||||||
{
|
{
|
||||||
pointerEvents: "none"
|
pointerEvents: "none"
|
||||||
},
|
}
|
||||||
height: "54px",
|
|
||||||
overflow: "hidden"
|
|
||||||
},
|
},
|
||||||
sorting:
|
sorting:
|
||||||
{
|
{
|
||||||
pointerEvents: "none",
|
pointerEvents: "none"
|
||||||
height: "54px",
|
|
||||||
overflow: "hidden"
|
|
||||||
},
|
},
|
||||||
dragging:
|
dragging:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -50,26 +50,30 @@ export default function CollectionView({ collection, index: collectionIndex, dra
|
|||||||
|
|
||||||
<CollectionHeader dragHandleProps={ activatorProps } dragHandleRef={ setActivatorNodeRef } />
|
<CollectionHeader dragHandleProps={ activatorProps } dragHandleRef={ setActivatorNodeRef } />
|
||||||
|
|
||||||
{ collection.items.length < 1 ?
|
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
|
||||||
<div className={ cls.empty }>
|
<>
|
||||||
<CollectionsRegular fontSize={ 32 } />
|
{ collection.items.length < 1 ?
|
||||||
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
|
<div className={ cls.empty }>
|
||||||
</div>
|
<CollectionsRegular fontSize={ 32 } />
|
||||||
:
|
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
|
||||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
|
</div>
|
||||||
<SortableContext
|
:
|
||||||
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
|
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
|
||||||
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
|
<SortableContext
|
||||||
>
|
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
|
||||||
{ collection.items.map((i, index) =>
|
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
|
||||||
i.type === "group" ?
|
>
|
||||||
<GroupView
|
{ collection.items.map((i, index) =>
|
||||||
key={ index } group={ i } indices={ [collectionIndex, index] } />
|
i.type === "group" ?
|
||||||
:
|
<GroupView
|
||||||
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
|
key={ index } group={ i } indices={ [collectionIndex, index] } />
|
||||||
) }
|
:
|
||||||
</SortableContext>
|
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
|
||||||
</div>
|
) }
|
||||||
|
</SortableContext>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
</div >
|
</div >
|
||||||
</CollectionContext.Provider>
|
</CollectionContext.Provider>
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
|||||||
|
|
||||||
const cls = useStyles_EditDialog();
|
const cls = useStyles_EditDialog();
|
||||||
const colorCls = useGroupColors();
|
const colorCls = useGroupColors();
|
||||||
|
const horizontalNavigationAttributes = fui.useArrowNavigationGroup({ axis: "horizontal" });
|
||||||
|
|
||||||
|
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = () =>
|
const handleSave = () =>
|
||||||
{
|
{
|
||||||
@@ -58,78 +65,80 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
|
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
|
||||||
<fui.DialogBody>
|
<form onSubmit={ onSubmit }>
|
||||||
<fui.DialogTitle>
|
<fui.DialogBody>
|
||||||
{
|
<fui.DialogTitle>
|
||||||
props.type === "collection" ?
|
{
|
||||||
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
|
props.type === "collection" ?
|
||||||
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
|
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
|
||||||
}
|
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
|
||||||
</fui.DialogTitle>
|
}
|
||||||
|
</fui.DialogTitle>
|
||||||
|
|
||||||
<fui.DialogContent>
|
<fui.DialogContent>
|
||||||
<form className={ cls.content }>
|
<div className={ cls.content }>
|
||||||
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
|
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
|
||||||
<fui.Input
|
<fui.Input
|
||||||
contentBefore={ <Rename20Regular /> }
|
contentBefore={ <Rename20Regular /> }
|
||||||
disabled={ color === "pinned" }
|
disabled={ color === "pinned" }
|
||||||
placeholder={
|
placeholder={
|
||||||
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
|
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
|
||||||
}
|
}
|
||||||
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
|
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
|
||||||
onChange={ (_, e) => setTitle(e.value) } />
|
onChange={ (_, e) => setTitle(e.value) } />
|
||||||
</fui.Field>
|
</fui.Field>
|
||||||
<fui.Field label="Color">
|
<fui.Field label="Color">
|
||||||
<div className={ cls.colorPicker }>
|
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
|
||||||
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
|
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
|
||||||
<fui.ToggleButton
|
<fui.ToggleButton
|
||||||
checked={ color === "pinned" }
|
checked={ color === "pinned" }
|
||||||
onClick={ () => setColor("pinned") }
|
onClick={ () => setColor("pinned") }
|
||||||
icon={ <Pin20Filled /> }
|
icon={ <Pin20Filled /> }
|
||||||
shape="circular"
|
shape="circular"
|
||||||
>
|
>
|
||||||
{ i18n.t("groups.pinned") }
|
{ i18n.t("groups.pinned") }
|
||||||
</fui.ToggleButton>
|
</fui.ToggleButton>
|
||||||
}
|
}
|
||||||
{ props.type === "collection" &&
|
{ props.type === "collection" &&
|
||||||
<fui.ToggleButton
|
<fui.ToggleButton
|
||||||
checked={ color === undefined }
|
checked={ color === undefined }
|
||||||
onClick={ () => setColor(undefined) }
|
onClick={ () => setColor(undefined) }
|
||||||
icon={ <CircleOff20Regular /> }
|
icon={ <CircleOff20Regular /> }
|
||||||
shape="circular"
|
shape="circular"
|
||||||
>
|
>
|
||||||
{ i18n.t("colors.none") }
|
{ i18n.t("colors.none") }
|
||||||
</fui.ToggleButton>
|
</fui.ToggleButton>
|
||||||
}
|
}
|
||||||
{ Object.keys(colorCls).map(i =>
|
{ Object.keys(colorCls).map(i =>
|
||||||
<fui.ToggleButton
|
<fui.ToggleButton
|
||||||
checked={ color === i }
|
checked={ color === i }
|
||||||
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
|
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
|
||||||
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
|
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
|
||||||
icon={ {
|
icon={ {
|
||||||
className: cls.colorButton_icon,
|
className: cls.colorButton_icon,
|
||||||
children: <Circle20Filled />
|
children: <Circle20Filled />
|
||||||
} }
|
} }
|
||||||
key={ i }
|
key={ i }
|
||||||
shape="circular"
|
shape="circular"
|
||||||
>
|
>
|
||||||
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
|
||||||
</fui.ToggleButton>
|
</fui.ToggleButton>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
</fui.Field>
|
</fui.Field>
|
||||||
</form>
|
</div>
|
||||||
</fui.DialogContent>
|
</fui.DialogContent>
|
||||||
|
|
||||||
<fui.DialogActions>
|
<fui.DialogActions>
|
||||||
<fui.DialogTrigger disableButtonEnhancement>
|
<fui.DialogTrigger disableButtonEnhancement>
|
||||||
<fui.Button appearance="primary" onClick={ handleSave }>{ i18n.t("common.actions.save") }</fui.Button>
|
<fui.Button appearance="primary" as="button" type="submit">{ i18n.t("common.actions.save") }</fui.Button>
|
||||||
</fui.DialogTrigger>
|
</fui.DialogTrigger>
|
||||||
<fui.DialogTrigger disableButtonEnhancement>
|
<fui.DialogTrigger disableButtonEnhancement>
|
||||||
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
|
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
|
||||||
</fui.DialogTrigger>
|
</fui.DialogTrigger>
|
||||||
</fui.DialogActions>
|
</fui.DialogActions>
|
||||||
</fui.DialogBody>
|
</fui.DialogBody>
|
||||||
|
</form>
|
||||||
</fui.DialogSurface>
|
</fui.DialogSurface>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,12 +99,23 @@ export const useStyles_GroupView = makeStyles({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
columnGap: tokens.spacingHorizontalS,
|
columnGap: tokens.spacingHorizontalS,
|
||||||
rowGap: tokens.spacingHorizontalSNudge,
|
rowGap: tokens.spacingHorizontalSNudge,
|
||||||
height: "100%"
|
height: "100%",
|
||||||
|
position: "relative"
|
||||||
},
|
},
|
||||||
verticalList:
|
verticalList:
|
||||||
{
|
{
|
||||||
flexFlow: "column"
|
flexFlow: "column"
|
||||||
},
|
},
|
||||||
|
verticalListCollapsed:
|
||||||
|
{
|
||||||
|
maxHeight: "136px",
|
||||||
|
overflow: "clip"
|
||||||
|
},
|
||||||
|
horizontalListCollapsed:
|
||||||
|
{
|
||||||
|
maxWidth: "400px",
|
||||||
|
overflow: "clip"
|
||||||
|
},
|
||||||
listContainer:
|
listContainer:
|
||||||
{
|
{
|
||||||
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`,
|
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`,
|
||||||
|
|||||||
@@ -88,7 +88,13 @@ export default function GroupView({ group, indices, dragOverlay }: GroupViewProp
|
|||||||
<Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong>
|
<Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
|
<div
|
||||||
|
className={ mergeClasses(
|
||||||
|
cls.list,
|
||||||
|
!tilesView && cls.verticalList,
|
||||||
|
((active?.item.type === "group" && active?.indices[0] === indices[0]) || dragOverlay) && (tilesView ? cls.horizontalListCollapsed : cls.verticalListCollapsed)
|
||||||
|
) }
|
||||||
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={ group.items.map((_, index) => [...indices, index].join("/")) }
|
items={ group.items.map((_, index) => [...indices, index].join("/")) }
|
||||||
disabled={ disableSorting }
|
disabled={ disableSorting }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const useStyles_TabView = makeStyles({
|
|||||||
|
|
||||||
width: "160px",
|
width: "160px",
|
||||||
height: "120px",
|
height: "120px",
|
||||||
|
flexShrink: 0,
|
||||||
marginBottom: tokens.spacingVerticalSNudge,
|
marginBottom: tokens.spacingVerticalSNudge,
|
||||||
|
|
||||||
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`,
|
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
|||||||
|
|
||||||
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
|
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
|
||||||
{
|
{
|
||||||
|
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||||
const [listLocation] = useSettings("listLocation");
|
const [listLocation] = useSettings("listLocation");
|
||||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||||
const { updateCollection } = useCollections();
|
const { updateCollection } = useCollections();
|
||||||
@@ -53,7 +54,7 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
|||||||
mergeClasses(
|
mergeClasses(
|
||||||
cls.toolbar,
|
cls.toolbar,
|
||||||
"CollectionView__toolbar",
|
"CollectionView__toolbar",
|
||||||
alwaysShowToolbars === true && cls.showToolbar
|
(alwaysShowToolbars === true || contextOpen) && cls.showToolbar
|
||||||
) }
|
) }
|
||||||
>
|
>
|
||||||
{ tabCount < 1 ?
|
{ tabCount < 1 ?
|
||||||
@@ -61,10 +62,10 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
|
|||||||
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
|
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
|
||||||
</Button>
|
</Button>
|
||||||
:
|
:
|
||||||
<OpenCollectionButton />
|
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
|
||||||
}
|
}
|
||||||
|
|
||||||
<CollectionMoreButton onAddSelected={ handleAddSelected } />
|
<CollectionMoreButton onAddSelected={ handleAddSelected } onOpenChange={ (_, e) => setContextOpen(e.open) } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useDialog } from "@/contexts/DialogProvider";
|
import { useDialog } from "@/contexts/DialogProvider";
|
||||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||||
import useSettings from "@/hooks/useSettings";
|
import useSettings from "@/hooks/useSettings";
|
||||||
import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
import { Button, Menu, MenuDivider, MenuItem, MenuList, MenuOpenChangeData, MenuOpenEvent, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
||||||
import * as ic from "@fluentui/react-icons";
|
import * as ic from "@fluentui/react-icons";
|
||||||
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
||||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||||
import exportCollectionToBookmarks from "../../utils/exportCollectionToBookmarks";
|
import exportCollectionToBookmarks from "../../utils/exportCollectionToBookmarks";
|
||||||
import EditDialog from "../EditDialog";
|
import EditDialog from "../EditDialog";
|
||||||
|
|
||||||
export default function CollectionMoreButton({ onAddSelected }: CollectionMoreButtonProps): React.ReactElement
|
export default function CollectionMoreButton({ onAddSelected, onOpenChange }: CollectionMoreButtonProps): React.ReactElement
|
||||||
{
|
{
|
||||||
const [listLocation] = useSettings("listLocation");
|
const [listLocation] = useSettings("listLocation");
|
||||||
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
const isTab: boolean = listLocation === "tab" || listLocation === "pinned";
|
||||||
@@ -56,7 +56,7 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu onOpenChange={ onOpenChange }>
|
||||||
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
|
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
|
||||||
<MenuTrigger>
|
<MenuTrigger>
|
||||||
<Button appearance="subtle" icon={ <ic.MoreVertical20Regular /> } />
|
<Button appearance="subtle" icon={ <ic.MoreVertical20Regular /> } />
|
||||||
@@ -94,4 +94,5 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
|
|||||||
export type CollectionMoreButtonProps =
|
export type CollectionMoreButtonProps =
|
||||||
{
|
{
|
||||||
onAddSelected?: () => void;
|
onAddSelected?: () => void;
|
||||||
|
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
|
|||||||
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
import { useDangerStyles } from "@/hooks/useDangerStyles";
|
||||||
import useSettings from "@/hooks/useSettings";
|
import useSettings from "@/hooks/useSettings";
|
||||||
import { TabItem } from "@/models/CollectionModels";
|
import { TabItem } from "@/models/CollectionModels";
|
||||||
|
import { sendMessage } from "@/utils/messaging";
|
||||||
|
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
||||||
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
|
||||||
import * as ic from "@fluentui/react-icons";
|
import * as ic from "@fluentui/react-icons";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
import { openGroup } from "../../utils/opener";
|
import { openGroup } from "../../utils/opener";
|
||||||
import saveTabsToCollection from "@/utils/saveTabsToCollection";
|
|
||||||
|
|
||||||
export default function GroupMoreMenu(): ReactElement
|
export default function GroupMoreMenu(): ReactElement
|
||||||
{
|
{
|
||||||
@@ -56,6 +57,14 @@ export default function GroupMoreMenu(): ReactElement
|
|||||||
onSave={ item => updateGroup(item, collection.timestamp, indices[1]) } />
|
onSave={ item => updateGroup(item, collection.timestamp, indices[1]) } />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openGroupInNewWindow = () =>
|
||||||
|
{
|
||||||
|
if (import.meta.env.FIREFOX && listLocation === "popup")
|
||||||
|
sendMessage("openGroup", { group, newWindow: true });
|
||||||
|
else
|
||||||
|
openGroup(group, true);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddSelected = async () =>
|
const handleAddSelected = async () =>
|
||||||
{
|
{
|
||||||
const newTabs: TabItem[] = isTab ?
|
const newTabs: TabItem[] = isTab ?
|
||||||
@@ -75,7 +84,7 @@ export default function GroupMoreMenu(): ReactElement
|
|||||||
<MenuPopover>
|
<MenuPopover>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
{ group.items.length > 0 &&
|
{ group.items.length > 0 &&
|
||||||
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => openGroup(group, true) }>
|
<MenuItem icon={ <NewWindowIcon /> } onClick={ openGroupInNewWindow }>
|
||||||
{ i18n.t("groups.menu.new_window") }
|
{ i18n.t("groups.menu.new_window") }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { useDialog } from "@/contexts/DialogProvider";
|
import { useDialog } from "@/contexts/DialogProvider";
|
||||||
import useSettings from "@/hooks/useSettings";
|
import useSettings from "@/hooks/useSettings";
|
||||||
import browserLocaleKey from "@/utils/browserLocaleKey";
|
import browserLocaleKey from "@/utils/browserLocaleKey";
|
||||||
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
|
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 * as ic from "@fluentui/react-icons";
|
||||||
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
import CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
|
||||||
import { useCollections } from "../../contexts/CollectionsProvider";
|
import { useCollections } from "../../contexts/CollectionsProvider";
|
||||||
import { openCollection } from "../../utils/opener";
|
import { openCollection } from "../../utils/opener";
|
||||||
|
|
||||||
export default function OpenCollectionButton(): React.ReactElement
|
export default function OpenCollectionButton({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement
|
||||||
{
|
{
|
||||||
const [defaultAction] = useSettings("defaultRestoreAction");
|
const [defaultAction] = useSettings("defaultRestoreAction");
|
||||||
|
const [listLocation] = useSettings("listLocation");
|
||||||
const { removeItem } = useCollections();
|
const { removeItem } = useCollections();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const { collection } = useContext<CollectionContextType>(CollectionContext);
|
const { collection } = useContext<CollectionContextType>(CollectionContext);
|
||||||
@@ -22,7 +24,12 @@ export default function OpenCollectionButton(): React.ReactElement
|
|||||||
const handleIncognito = async () =>
|
const handleIncognito = async () =>
|
||||||
{
|
{
|
||||||
if (await browser.extension.isAllowedIncognitoAccess())
|
if (await browser.extension.isAllowedIncognitoAccess())
|
||||||
openCollection(collection, "incognito");
|
{
|
||||||
|
if (import.meta.env.FIREFOX && listLocation === "popup")
|
||||||
|
sendMessage("openCollection", { collection, targetWindow: "incognito" });
|
||||||
|
else
|
||||||
|
openCollection(collection, "incognito");
|
||||||
|
}
|
||||||
else
|
else
|
||||||
dialog.pushPrompt({
|
dialog.pushPrompt({
|
||||||
title: i18n.t("collections.incognito_check.title"),
|
title: i18n.t("collections.incognito_check.title"),
|
||||||
@@ -45,7 +52,9 @@ export default function OpenCollectionButton(): React.ReactElement
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpen = (mode: "current" | "new") =>
|
const handleOpen = (mode: "current" | "new") =>
|
||||||
() => openCollection(collection, mode);
|
import.meta.env.FIREFOX && listLocation === "popup" && mode === "new" ?
|
||||||
|
() => sendMessage("openCollection", { collection, targetWindow: "new" }) :
|
||||||
|
() => openCollection(collection, mode);
|
||||||
|
|
||||||
const handleRestore = async () =>
|
const handleRestore = async () =>
|
||||||
{
|
{
|
||||||
@@ -54,7 +63,7 @@ export default function OpenCollectionButton(): React.ReactElement
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu onOpenChange={ onOpenChange }>
|
||||||
<MenuTrigger disableButtonEnhancement>
|
<MenuTrigger disableButtonEnhancement>
|
||||||
{ (triggerProps: MenuButtonProps) => defaultAction === "restore" ?
|
{ (triggerProps: MenuButtonProps) => defaultAction === "restore" ?
|
||||||
<SplitButton
|
<SplitButton
|
||||||
@@ -95,3 +104,8 @@ export default function OpenCollectionButton(): React.ReactElement
|
|||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OpenCollectionButtonProps =
|
||||||
|
{
|
||||||
|
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
|
||||||
|
};
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ export const useStyles_CollectionListView = makeStyles({
|
|||||||
listView:
|
listView:
|
||||||
{
|
{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
|
|
||||||
|
"@media screen and (min-width: 360px)":
|
||||||
|
{
|
||||||
|
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { collisionDetector } from "../../utils/dnd/collisionDetector";
|
|||||||
import { useStyles_CollectionListView } from "./CollectionListView.styles";
|
import { useStyles_CollectionListView } from "./CollectionListView.styles";
|
||||||
import SearchBar from "./SearchBar";
|
import SearchBar from "./SearchBar";
|
||||||
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
|
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
|
||||||
|
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
|
||||||
|
|
||||||
export default function CollectionListView(): ReactElement
|
export default function CollectionListView(): ReactElement
|
||||||
{
|
{
|
||||||
@@ -110,6 +111,7 @@ export default function CollectionListView(): ReactElement
|
|||||||
collisionDetection={ collisionDetector(!tilesView) }
|
collisionDetection={ collisionDetector(!tilesView) }
|
||||||
onDragStart={ handleDragStart }
|
onDragStart={ handleDragStart }
|
||||||
onDragEnd={ handleDragEnd }
|
onDragEnd={ handleDragEnd }
|
||||||
|
modifiers={ [snapHandleToCursor] }
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
items={ resultList.map((_, index) => index.toString()) }
|
items={ resultList.map((_, index) => index.toString()) }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import useStorageInfo from "@/hooks/useStorageInfo";
|
import useStorageInfo from "@/hooks/useStorageInfo";
|
||||||
import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
import { MessageBar, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
|
||||||
|
|
||||||
export default function StorageCapacityIssueMessage(props: MessageBarProps): JSX.Element
|
export default function StorageCapacityIssueMessage(props: MessageBarProps): React.ReactElement
|
||||||
{
|
{
|
||||||
const { usedStorageRatio } = useStorageInfo();
|
const { usedStorageRatio } = useStorageInfo();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import App from "@/App.tsx";
|
import App from "@/App.tsx";
|
||||||
import "@/assets/global.css";
|
import "@/assets/global.css";
|
||||||
|
import { trackPage } from "@/features/analytics";
|
||||||
import { useLocalMigration } from "@/features/migration";
|
import { useLocalMigration } from "@/features/migration";
|
||||||
import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog";
|
import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog";
|
||||||
import { Divider, makeStyles } from "@fluentui/react-components";
|
import { Divider, makeStyles } from "@fluentui/react-components";
|
||||||
@@ -7,6 +8,8 @@ import ReactDOM from "react-dom/client";
|
|||||||
import CollectionsProvider from "./contexts/CollectionsProvider";
|
import CollectionsProvider from "./contexts/CollectionsProvider";
|
||||||
import CollectionListView from "./layouts/collections/CollectionListView";
|
import CollectionListView from "./layouts/collections/CollectionListView";
|
||||||
import Header from "./layouts/header/Header";
|
import Header from "./layouts/header/Header";
|
||||||
|
import { useSettingsReviewDialog } from "@/features/settingsReview";
|
||||||
|
import useDialogTrain from "@/hooks/useDialogTrain";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<App>
|
<App>
|
||||||
@@ -15,14 +18,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
);
|
);
|
||||||
|
|
||||||
document.title = i18n.t("manifest.name");
|
document.title = i18n.t("manifest.name");
|
||||||
analytics.page("collection_list");
|
trackPage("collection_list");
|
||||||
|
|
||||||
function MainPage(): React.ReactElement
|
function MainPage(): React.ReactElement
|
||||||
{
|
{
|
||||||
const cls = useStyles();
|
const cls = useStyles();
|
||||||
|
|
||||||
useLocalMigration();
|
useLocalMigration();
|
||||||
useWelcomeDialog();
|
useDialogTrain(
|
||||||
|
useWelcomeDialog,
|
||||||
|
useSettingsReviewDialog
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionsProvider>
|
<CollectionsProvider>
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
|
|||||||
|
|
||||||
if (activeItem.item.type === "collection")
|
if (activeItem.item.type === "collection")
|
||||||
{
|
{
|
||||||
|
// If we drag a collection, we should ignore other items, like tabs or groups
|
||||||
if (droppableItem.item.type !== "collection")
|
if (droppableItem.item.type !== "collection")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Using distance between centers
|
||||||
value = distanceBetween(centerOfRectangle(rect), centerRect);
|
value = distanceBetween(centerOfRectangle(rect), centerRect);
|
||||||
collisions.push({ id, data: { droppableContainer, value } });
|
collisions.push({ id, data: { droppableContainer, value } });
|
||||||
continue;
|
continue;
|
||||||
@@ -44,14 +46,20 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
|
|||||||
const intersectionRatio: number = getIntersectionRatio(rect, collisionRect);
|
const intersectionRatio: number = getIntersectionRatio(rect, collisionRect);
|
||||||
const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect);
|
const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect);
|
||||||
|
|
||||||
|
// Dragging a tab or a group over a collection
|
||||||
if (droppableItem.item.type === "collection")
|
if (droppableItem.item.type === "collection")
|
||||||
{
|
{
|
||||||
|
// Ignoring collection, if the tab or the group is inside that collection
|
||||||
if (activeItem.indices.length === 2 && activeItem.indices[0] === droppableItem.indices[0])
|
if (activeItem.indices.length === 2 && activeItem.indices[0] === droppableItem.indices[0])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (intersectionCoefficient < 0.7 && activeItem.item.type === "tab")
|
// Ignoring collection if we're dragging a tab or a group that doesn't belong to the collection,
|
||||||
|
// but intersection ratio is less than 0.7
|
||||||
|
if (intersectionCoefficient < 0.7)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// If we're dragging a tab, that's inside a group that belongs to the collection,
|
||||||
|
// we substract the group's intersection from the collection's one
|
||||||
if (activeItem.indices.length === 3 && activeItem.indices[0] === droppableItem.indices[0])
|
if (activeItem.indices.length === 3 && activeItem.indices[0] === droppableItem.indices[0])
|
||||||
{
|
{
|
||||||
const [collectionId, groupId] = activeItem.indices;
|
const [collectionId, groupId] = activeItem.indices;
|
||||||
@@ -62,16 +70,23 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
|
|||||||
|
|
||||||
value = 1 / (intersectionRatio - getIntersectionRatio(groupRect, collisionRect));
|
value = 1 / (intersectionRatio - getIntersectionRatio(groupRect, collisionRect));
|
||||||
}
|
}
|
||||||
|
// Otherwise, use intersection ratio
|
||||||
|
// At this point we're dragging either:
|
||||||
|
// - a group, that doesn't belong to the collection
|
||||||
|
// - a tab, that either belongs to the collection's group, or has intersection coefficient >= .7
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
value = 1 / intersectionRatio;
|
value = 2 / intersectionRatio;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If we're dragging a tab or a group over another group's dropzone
|
||||||
else if (droppableItem.item.type === "group" && (id as string).endsWith("_dropzone"))
|
else if (droppableItem.item.type === "group" && (id as string).endsWith("_dropzone"))
|
||||||
{
|
{
|
||||||
|
// Ignore, if we're dragging a group
|
||||||
if (activeItem.item.type === "group")
|
if (activeItem.item.type === "group")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Ignore, if we're dragging a tab, that's inside the group
|
||||||
if (
|
if (
|
||||||
activeItem.indices.length === 3 &&
|
activeItem.indices.length === 3 &&
|
||||||
activeItem.indices[0] === droppableItem.indices[0] &&
|
activeItem.indices[0] === droppableItem.indices[0] &&
|
||||||
@@ -79,11 +94,15 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
|
|||||||
)
|
)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Ignore, if coefficient is less than .5
|
||||||
|
// (at this point we're dragging a tab, that's outside of the group's dropzone)
|
||||||
if (intersectionCoefficient < 0.5)
|
if (intersectionCoefficient < 0.5)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Use intersection between the tab and the group's dropzone
|
||||||
value = 1 / intersectionRatio;
|
value = 1 / intersectionRatio;
|
||||||
}
|
}
|
||||||
|
// We're dragging a group or a tab over its sibling
|
||||||
else if (activeItem.indices.length === droppableItem.indices.length)
|
else if (activeItem.indices.length === droppableItem.indices.length)
|
||||||
{
|
{
|
||||||
if (activeItem.indices[0] !== droppableItem.indices[0])
|
if (activeItem.indices[0] !== droppableItem.indices[0])
|
||||||
@@ -92,9 +111,22 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
|
|||||||
if (activeItem.indices.length === 3 && activeItem.indices[1] !== droppableItem.indices[1])
|
if (activeItem.indices.length === 3 && activeItem.indices[1] !== droppableItem.indices[1])
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Ignore pinned groups
|
||||||
if (droppableItem.item.type === "group" && droppableItem.item.pinned === true)
|
if (droppableItem.item.type === "group" && droppableItem.item.pinned === true)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
const collectionRect: ClientRect | undefined = droppableRects.get(activeItem.indices[0].toString());
|
||||||
|
|
||||||
|
if (!collectionRect)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const collectionIntersectionRatio: number = getIntersectionRatio(collectionRect, collisionRect);
|
||||||
|
const collectionIntersectionCoefficient: number = collectionIntersectionRatio / getMaxIntersectionRatio(collectionRect, collisionRect);
|
||||||
|
|
||||||
|
// Ignore if we are outside of the home collection
|
||||||
|
if (collectionIntersectionCoefficient < 0.7)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (activeItem.item.type === "tab" && droppableItem.item.type === "tab")
|
if (activeItem.item.type === "tab" && droppableItem.item.type === "tab")
|
||||||
{
|
{
|
||||||
value = distanceBetween(centerOfRectangle(rect), centerRect);
|
value = distanceBetween(centerOfRectangle(rect), centerRect);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Modifier } from "@dnd-kit/core";
|
||||||
|
import { Coordinates, getEventCoordinates } from "@dnd-kit/utilities";
|
||||||
|
import { DndItem } from "../../hooks/useDndItem";
|
||||||
|
|
||||||
|
export const snapHandleToCursor: Modifier = ({
|
||||||
|
activatorEvent,
|
||||||
|
draggingNodeRect,
|
||||||
|
transform,
|
||||||
|
active
|
||||||
|
}) =>
|
||||||
|
{
|
||||||
|
if (draggingNodeRect && activatorEvent)
|
||||||
|
{
|
||||||
|
const activeItem: DndItem | undefined = active?.data.current as DndItem;
|
||||||
|
const activatorCoordinates: Coordinates | null = getEventCoordinates(activatorEvent);
|
||||||
|
|
||||||
|
if (!activatorCoordinates)
|
||||||
|
return transform;
|
||||||
|
|
||||||
|
const initX: number = activatorCoordinates.x - draggingNodeRect.left;
|
||||||
|
const initY: number = activatorCoordinates.y - draggingNodeRect.top;
|
||||||
|
|
||||||
|
const offsetX: number = activeItem?.item.type === "group" ? 24 : draggingNodeRect.height / 2;
|
||||||
|
const offsetY: number = activeItem?.item.type === "group" ? 20 : draggingNodeRect.height / 2;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...transform,
|
||||||
|
x: transform.x + initX - offsetX,
|
||||||
|
y: transform.y + initY - offsetY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return transform;
|
||||||
|
};
|
||||||
@@ -1,10 +1,21 @@
|
|||||||
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
import { CollectionItem, TabItem } from "@/models/CollectionModels";
|
||||||
import sendNotification from "@/utils/sendNotification";
|
import sendNotification from "@/utils/sendNotification";
|
||||||
import { Bookmarks } from "wxt/browser";
|
import { Bookmarks, Permissions } from "wxt/browser";
|
||||||
import { getCollectionTitle } from "./getCollectionTitle";
|
import { getCollectionTitle } from "./getCollectionTitle";
|
||||||
|
import { track } from "@/features/analytics";
|
||||||
|
|
||||||
export default async function exportCollectionToBookmarks(collection: CollectionItem)
|
export default async function exportCollectionToBookmarks(collection: CollectionItem)
|
||||||
{
|
{
|
||||||
|
const permissions: Permissions.AnyPermissions = await browser.permissions.getAll();
|
||||||
|
|
||||||
|
if (!permissions.permissions?.includes("bookmarks"))
|
||||||
|
{
|
||||||
|
const granted: boolean = await browser.permissions.request({ permissions: ["bookmarks"] });
|
||||||
|
|
||||||
|
if (!granted)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
|
||||||
title: getCollectionTitle(collection)
|
title: getCollectionTitle(collection)
|
||||||
});
|
});
|
||||||
@@ -31,6 +42,8 @@ export default async function exportCollectionToBookmarks(collection: Collection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
track("bookmarks_saved");
|
||||||
|
|
||||||
await sendNotification({
|
await sendNotification({
|
||||||
title: i18n.t("notifications.bookmark_saved.title"),
|
title: i18n.t("notifications.bookmark_saved.title"),
|
||||||
message: i18n.t("notifications.bookmark_saved.message"),
|
message: i18n.t("notifications.bookmark_saved.message"),
|
||||||
|
|||||||
@@ -1,8 +1,24 @@
|
|||||||
import { TabItem } from "@/models/CollectionModels";
|
import { TabItem } from "@/models/CollectionModels";
|
||||||
|
import sendNotification from "@/utils/sendNotification";
|
||||||
import { Tabs } from "wxt/browser";
|
import { Tabs } from "wxt/browser";
|
||||||
|
|
||||||
export default async function getSelectedTabs(): Promise<TabItem[]>
|
export default async function getSelectedTabs(): Promise<TabItem[]>
|
||||||
{
|
{
|
||||||
const tabs: Tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
|
let tabs: Tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
|
||||||
return tabs.filter(i => i.url).map(i => ({ type: "tab", url: i.url!, title: i.title }));
|
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"
|
||||||
|
});
|
||||||
|
|
||||||
|
return tabs.map(i => ({ type: "tab", url: i.url!, title: i.title }));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ export default defineConfig([
|
|||||||
"@stylistic/semi": ["error", "always"],
|
"@stylistic/semi": ["error", "always"],
|
||||||
"@stylistic/block-spacing": ["warn", "always"],
|
"@stylistic/block-spacing": ["warn", "always"],
|
||||||
"@stylistic/arrow-spacing": ["warn", { before: true, after: true }],
|
"@stylistic/arrow-spacing": ["warn", { before: true, after: true }],
|
||||||
"@stylistic/indent": ["warn", "tab"],
|
"@stylistic/indent": ["warn", "tab", { assignmentOperator: "off" }],
|
||||||
"@stylistic/quotes": ["error", "double"],
|
"@stylistic/quotes": ["error", "double"],
|
||||||
"@stylistic/comma-spacing": ["warn"],
|
"@stylistic/comma-spacing": ["warn"],
|
||||||
"@stylistic/comma-dangle": ["warn", "never"],
|
"@stylistic/comma-dangle": ["warn", "never"],
|
||||||
|
|||||||
@@ -1,3 +1,55 @@
|
|||||||
export { default as userPropertiesStorage } from "./utils/userPropertiesStorage";
|
import { analytics } from "./utils/analytics";
|
||||||
export { default as trackError } from "./utils/trackError";
|
import analyticsPermission from "./utils/analyticsPermission";
|
||||||
export { default as track } from "./utils/track";
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/storage";
|
||||||
|
import { analytics } from "./analytics";
|
||||||
|
import { Permissions } from "wxt/browser";
|
||||||
|
|
||||||
|
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"]
|
||||||
|
})
|
||||||
|
: 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"]
|
||||||
|
});
|
||||||
|
else
|
||||||
|
result = await browser.permissions.remove({
|
||||||
|
data_collection: ["technicalAndInteraction"]
|
||||||
|
});
|
||||||
|
|
||||||
|
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: Permissions.Permissions): Promise<void> =>
|
||||||
|
{
|
||||||
|
if (permissions.data_collection?.includes("technicalAndInteraction"))
|
||||||
|
{
|
||||||
|
const isGranted: boolean = await browser.permissions.contains({ data_collection: ["technicalAndInteraction"] });
|
||||||
|
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
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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,6 +5,9 @@ export { default as getCollections } from "./utils/getCollections";
|
|||||||
export { default as resoveConflict } from "./utils/resolveConflict";
|
export { default as resoveConflict } from "./utils/resolveConflict";
|
||||||
export { default as saveCollections } from "./utils/saveCollections";
|
export { default as saveCollections } from "./utils/saveCollections";
|
||||||
export { default as setCloudStorage } from "./utils/setCloudStorage";
|
export { default as setCloudStorage } from "./utils/setCloudStorage";
|
||||||
|
export { default as clearGraphicsStorage } from "./utils/clearGraphics";
|
||||||
|
|
||||||
|
export { default as thumbnailCaptureEnabled } from "./utils/thumbnailCaptureEnabled";
|
||||||
|
|
||||||
export const collectionCount = collectionStorage.count;
|
export const collectionCount = collectionStorage.count;
|
||||||
export const graphics = collectionStorage.graphics;
|
export const graphics = collectionStorage.graphics;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { collectionStorage } from "./collectionStorage";
|
||||||
|
|
||||||
|
export default async function clearGraphicsStorage(): Promise<void>
|
||||||
|
{
|
||||||
|
await collectionStorage.graphics.removeValue();
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ export default async function getCollectionsFromCloud(): Promise<CollectionItem[
|
|||||||
const chunks: Record<string, string> =
|
const chunks: Record<string, string> =
|
||||||
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
|
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
|
||||||
|
|
||||||
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "StorageBinaryString" });
|
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "Base64" });
|
||||||
|
|
||||||
return parseCollections(data);
|
return parseCollections(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default async function saveCollectionsToCloud(collections: CollectionItem
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: string = compress(serializeCollections(collections), { outputEncoding: "StorageBinaryString" });
|
const data: string = compress(serializeCollections(collections), { outputEncoding: "Base64" });
|
||||||
const chunks: string[] = splitIntoChunks(data);
|
const chunks: string[] = splitIntoChunks(data);
|
||||||
|
|
||||||
if (chunks.length > collectionStorage.maxChunkCount)
|
if (chunks.length > collectionStorage.maxChunkCount)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Permissions } from "wxt/browser";
|
||||||
|
import { Unwatch, WatchCallback, WxtStorageItem } from "wxt/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: 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;
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { githubLinks } from "@/data/links";
|
||||||
|
import { analyticsPermission } from "@/features/analytics";
|
||||||
|
import extLink from "@/utils/extLink";
|
||||||
|
import * as fui from "@fluentui/react-components";
|
||||||
|
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||||
|
import { reviewSettings } from "../utils/setSettingsReviewNeeded";
|
||||||
|
import { Unwatch } from "wxt/storage";
|
||||||
|
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||||
|
import SettingsReviewDialog from "../components/SettingsReviewDialog";
|
||||||
|
import { settingsForReview } from "../utils/showSettingsReviewDialog";
|
||||||
|
|
||||||
|
export default function useSettingsReviewDialog(dialog: DialogContextType): Promise<void>
|
||||||
|
{
|
||||||
|
return new Promise<void>(res =>
|
||||||
|
{
|
||||||
|
settingsForReview.getValue().then(needsReview =>
|
||||||
|
{
|
||||||
|
if (needsReview.length > 0)
|
||||||
|
dialog.pushCustom(
|
||||||
|
<SettingsReviewDialog />,
|
||||||
|
undefined,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
settingsForReview.removeValue();
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
else
|
||||||
|
res();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as useSettingsReviewDialog } from "./hooks/useSettingsReviewDialog";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as setSettingsReviewNeeded } from "./setSettingsReviewNeeded";
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { analyticsPermission } from "@/features/analytics";
|
||||||
|
import { Runtime } from "wxt/browser";
|
||||||
|
import { settingsForReview } from "./showSettingsReviewDialog";
|
||||||
|
|
||||||
|
export default async function setSettingsReviewNeeded(installReason: 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: 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: 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const settingsForReview = storage.defineItem<string[]>(
|
||||||
|
"local:settingsForReview",
|
||||||
|
{
|
||||||
|
fallback: []
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
import { useDialog } from "@/contexts/DialogProvider";
|
import { DialogContextType } from "@/contexts/DialogProvider";
|
||||||
import WelcomeDialog from "../components/WelcomeDialog";
|
import WelcomeDialog from "../components/WelcomeDialog";
|
||||||
import { showWelcomeDialog } from "../utils/showWelcomeDialog";
|
import { showWelcomeDialog } from "../utils/showWelcomeDialog";
|
||||||
|
|
||||||
export default function useWelcomeDialog(): void
|
export default function useWelcomeDialog(dialog: DialogContextType): Promise<void>
|
||||||
{
|
{
|
||||||
const dialog = useDialog();
|
return new Promise<void>(res =>
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
{
|
||||||
showWelcomeDialog.getValue().then(showWelcome =>
|
showWelcomeDialog.getValue().then(showWelcome =>
|
||||||
{
|
{
|
||||||
if (showWelcome || import.meta.env.DEV)
|
if (showWelcome || import.meta.env.DEV)
|
||||||
dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
|
dialog.pushCustom(
|
||||||
|
<WelcomeDialog />,
|
||||||
|
undefined,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
showWelcomeDialog.removeValue();
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
else
|
||||||
|
res();
|
||||||
});
|
});
|
||||||
}, []);
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "Visit our dev blog to learn more about this update and all of its features!"
|
text3: "Visit our dev blog to learn more about this update and all of its features!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Read dev blog"
|
visit_blog: "Read dev blog"
|
||||||
|
settingsReview:
|
||||||
|
title: "Review your settings"
|
||||||
|
action: "All settings"
|
||||||
|
analytics:
|
||||||
|
title: "These statistics will help us improve the extension"
|
||||||
|
p1: "We only collect usage statistics (number of collections, used features, etc.)"
|
||||||
|
p2: "We do not collect any of your data!"
|
||||||
|
p3_text: "See the full list of what we collect"
|
||||||
|
p3_link: "here"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Show counter badge"
|
show_badge: "Show counter badge"
|
||||||
show_notification: "Show notification when saving tabs using context menu"
|
show_notification: "Show notification when saving tabs using context menu"
|
||||||
unload_tabs: "Do not load tabs after opening"
|
unload_tabs: "Do not load tabs after opening"
|
||||||
|
allow_analytics: "Allow collection of anonymous statistics"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Open collection list in:"
|
title: "Open collection list in:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized."
|
text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized."
|
||||||
action: "Disable and reload the extension"
|
action: "Disable and reload the extension"
|
||||||
|
thumbnail_capture: "Capture thumbnails and icons for saved tabs"
|
||||||
|
thumbnail_capture_notice1: "Requires permission to access content on visited websites"
|
||||||
|
thumbnail_capture_notice2: "Disabling this feature may improve performance on large collections"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Clear saved thumbnails"
|
||||||
|
title: "Delete all saved thumbnails?"
|
||||||
|
prompt: "This action will remove all saved thumbnails, previews and icons for your saved tabs. This action cannot be undone."
|
||||||
about:
|
about:
|
||||||
title: "About"
|
title: "About"
|
||||||
developed_by: "Developed by Eugene Fox"
|
developed_by: "Developed by Eugene Fox"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "My website"
|
website: "My website"
|
||||||
source: "Source code"
|
source: "Source code"
|
||||||
changelog: "Changelog"
|
changelog: "Changelog"
|
||||||
|
privacy: "Privacy policy"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "This collection is empty"
|
empty: "This collection is empty"
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!"
|
text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Leer el blog de desarrollo"
|
visit_blog: "Leer el blog de desarrollo"
|
||||||
|
settingsReview:
|
||||||
|
title: "Revisa tus ajustes"
|
||||||
|
action: "Todos los ajustes"
|
||||||
|
analytics:
|
||||||
|
title: "Estas estadísticas nos ayudarán a mejorar la extensión"
|
||||||
|
p1: "Solo recopilamos estadísticas de uso (número de colecciones, funciones utilizadas, etc.)"
|
||||||
|
p2: "¡No recopilamos ninguno de tus datos!"
|
||||||
|
p3_text: "Ver la lista completa de lo que recopilamos"
|
||||||
|
p3_link: "aquí"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Mostrar insignia de contador"
|
show_badge: "Mostrar insignia de contador"
|
||||||
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
|
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
|
||||||
unload_tabs: "No cargar pestañas después de abrir"
|
unload_tabs: "No cargar pestañas después de abrir"
|
||||||
|
allow_analytics: "Permitir la recopilación de estadísticas anónimas"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Abrir lista de colecciones en:"
|
title: "Abrir lista de colecciones en:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "Esta acción deshabilitará la sincronización de colecciones entre tus dispositivos. La configuración de la extensión seguirá sincronizándose."
|
text: "Esta acción deshabilitará la sincronización de colecciones entre tus dispositivos. La configuración de la extensión seguirá sincronizándose."
|
||||||
action: "Deshabilitar y recargar la extensión"
|
action: "Deshabilitar y recargar la extensión"
|
||||||
|
thumbnail_capture: "Capturar miniaturas e íconos para las pestañas guardadas"
|
||||||
|
thumbnail_capture_notice1: "Requiere permiso para acceder al contenido de los sitios web visitados"
|
||||||
|
thumbnail_capture_notice2: "Deshabilitar esta función puede mejorar el rendimiento en colecciones grandes"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Eliminar miniaturas guardadas"
|
||||||
|
title: "¿Eliminar todas las miniaturas guardadas?"
|
||||||
|
prompt: "Esta acción eliminará todas las miniaturas, vistas previas e íconos guardados para tus pestañas guardadas. Esta acción no se puede deshacer."
|
||||||
about:
|
about:
|
||||||
title: "Acerca de"
|
title: "Acerca de"
|
||||||
developed_by: "Desarrollado por Eugene Fox"
|
developed_by: "Desarrollado por Eugene Fox"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "Mi sitio web"
|
website: "Mi sitio web"
|
||||||
source: "Código fuente"
|
source: "Código fuente"
|
||||||
changelog: "Registro de cambios"
|
changelog: "Registro de cambios"
|
||||||
|
privacy: "Política de privacidad"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "Esta colección está vacía"
|
empty: "Esta colección está vacía"
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!"
|
text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Leggi il blog degli sviluppatori"
|
visit_blog: "Leggi il blog degli sviluppatori"
|
||||||
|
settingsReview:
|
||||||
|
title: "Rivedi le tue impostazioni"
|
||||||
|
action: "Tutte le impostazioni"
|
||||||
|
analytics:
|
||||||
|
title: "Queste statistiche ci aiuteranno a migliorare l'estensione"
|
||||||
|
p1: "Raccogliamo solo statistiche di utilizzo (numero di collezioni, funzionalità utilizzate, ecc.)"
|
||||||
|
p2: "Non raccogliamo nessuno dei tuoi dati!"
|
||||||
|
p3_text: "Vedi l'elenco completo di ciò che raccogliamo"
|
||||||
|
p3_link: "qui"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Mostra il badge del contatore"
|
show_badge: "Mostra il badge del contatore"
|
||||||
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
|
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
|
||||||
unload_tabs: "Non caricare le schede dopo l'apertura"
|
unload_tabs: "Non caricare le schede dopo l'apertura"
|
||||||
|
allow_analytics: "Consenti la raccolta di statistiche anonime"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Apri elenco delle collezioni in:"
|
title: "Apri elenco delle collezioni in:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "Questa azione disabiliterà la sincronizzazione delle collezioni tra i tuoi dispositivi. Le impostazioni dell'estensione saranno comunque sincronizzate."
|
text: "Questa azione disabiliterà la sincronizzazione delle collezioni tra i tuoi dispositivi. Le impostazioni dell'estensione saranno comunque sincronizzate."
|
||||||
action: "Disabilita e ricarica l'estensione"
|
action: "Disabilita e ricarica l'estensione"
|
||||||
|
thumbnail_capture: "Cattura miniature e icone per le schede salvate"
|
||||||
|
thumbnail_capture_notice1: "Richiede il permesso di accedere ai contenuti dei siti web visitati"
|
||||||
|
thumbnail_capture_notice2: "Disabilitare questa funzione può migliorare le prestazioni su collezioni di grandi dimensioni"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Elimina miniature salvate"
|
||||||
|
title: "Eliminare tutte le miniature salvate?"
|
||||||
|
prompt: "Questa azione rimuoverà tutte le miniature, anteprime e icone salvate per le tue schede salvate. Questa azione non può essere annullata."
|
||||||
about:
|
about:
|
||||||
title: "Informazioni"
|
title: "Informazioni"
|
||||||
developed_by: "Sviluppato da Eugene Fox"
|
developed_by: "Sviluppato da Eugene Fox"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "Il mio sito web"
|
website: "Il mio sito web"
|
||||||
source: "Codice sorgente"
|
source: "Codice sorgente"
|
||||||
changelog: "Registro delle modifiche"
|
changelog: "Registro delle modifiche"
|
||||||
|
privacy: "Politica sulla riservatezza"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "Questa collezione è vuota"
|
empty: "Questa collezione è vuota"
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "Odwiedź blog dewelopera (tylko w języku angielskim), aby dowiedzieć się więcej o tej aktualizacji i wszystkich jej funkcjach!"
|
text3: "Odwiedź blog dewelopera (tylko w języku angielskim), aby dowiedzieć się więcej o tej aktualizacji i wszystkich jej funkcjach!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Czytaj blog"
|
visit_blog: "Czytaj blog"
|
||||||
|
settingsReview:
|
||||||
|
title: "Sprawdź ustawienia"
|
||||||
|
action: "Wszystkie ustawienia"
|
||||||
|
analytics:
|
||||||
|
title: "Ta statystyka pozwoli ulepszać rozszerzenie"
|
||||||
|
p1: "Zbieramy tylko statystyki użycia (liczba kolekcji, używane funkcje itp.)"
|
||||||
|
p2: "Nie zbieramy twoich danych osobowych!"
|
||||||
|
p3_text: "Pełną listę zbieranych danych można zobaczyć"
|
||||||
|
p3_link: "tutaj"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Pokaż licznik"
|
show_badge: "Pokaż licznik"
|
||||||
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
|
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
|
||||||
unload_tabs: "Nie ładuj kart po otwarciu"
|
unload_tabs: "Nie ładuj kart po otwarciu"
|
||||||
|
allow_analytics: "Zezwól na zbieranie anonimowej statystyki"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Otwieraj listę kolekcji w:"
|
title: "Otwieraj listę kolekcji w:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "Ta akcja wyłączy synchronizację kolekcji między twoimi urządzeniami. Ustawienia nadal będą przechowywane w chmurze."
|
text: "Ta akcja wyłączy synchronizację kolekcji między twoimi urządzeniami. Ustawienia nadal będą przechowywane w chmurze."
|
||||||
action: "Wyłącz i przeładuj rozszerzenie"
|
action: "Wyłącz i przeładuj rozszerzenie"
|
||||||
|
thumbnail_capture: "Zapisuj podglądy i ikony dla zapisanych kart"
|
||||||
|
thumbnail_capture_notice1: "Wymagany dostęp do zawartości odwiedzanych stron internetowych"
|
||||||
|
thumbnail_capture_notice2: "Wyłączenie tej funkcji może poprawić wydajność przy dużej liczbie zapisanych kart"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Usuń zapisane ikony"
|
||||||
|
title: "Usunąć podglądy i ikony?"
|
||||||
|
prompt: "Ta akcja usunie wszystkie podglądy i ikony twoich zapisanych kart. Tej akcji nie można cofnąć."
|
||||||
about:
|
about:
|
||||||
title: "O rozszerzeniu"
|
title: "O rozszerzeniu"
|
||||||
developed_by: "Wywoływacz: Eugeniusz Lis"
|
developed_by: "Wywoływacz: Eugeniusz Lis"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "Moja strona internetowa"
|
website: "Moja strona internetowa"
|
||||||
source: "Kod źródłowy"
|
source: "Kod źródłowy"
|
||||||
changelog: "Lista zmian"
|
changelog: "Lista zmian"
|
||||||
|
privacy: "Polityka prywatności"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "Ta kolekcja jest pusta"
|
empty: "Ta kolekcja jest pusta"
|
||||||
|
|||||||
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!"
|
text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Ler blog de desenvolvimento"
|
visit_blog: "Ler blog de desenvolvimento"
|
||||||
|
settingsReview:
|
||||||
|
title: "Revise suas configurações"
|
||||||
|
action: "Todas as configurações"
|
||||||
|
analytics:
|
||||||
|
title: "Estas estatísticas nos ajudarão a melhorar a extensão"
|
||||||
|
p1: "Nós coletamos apenas estatísticas de uso (número de coleções, recursos usados, etc.)"
|
||||||
|
p2: "Nós não coletamos nenhum dos seus dados!"
|
||||||
|
p3_text: "Veja a lista completa do que coletamos"
|
||||||
|
p3_link: "aqui"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Mostrar contador no ícone"
|
show_badge: "Mostrar contador no ícone"
|
||||||
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
|
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
|
||||||
unload_tabs: "Não carregar abas após abrir"
|
unload_tabs: "Não carregar abas após abrir"
|
||||||
|
allow_analytics: "Permitir coleta de estatísticas anônimas"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Abrir lista de coleções em:"
|
title: "Abrir lista de coleções em:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "Esta ação desativará a sincronização de coleções entre seus dispositivos. As configurações da extensão ainda serão sincronizadas."
|
text: "Esta ação desativará a sincronização de coleções entre seus dispositivos. As configurações da extensão ainda serão sincronizadas."
|
||||||
action: "Desativar e recarregar a extensão"
|
action: "Desativar e recarregar a extensão"
|
||||||
|
thumbnail_capture: "Capturar miniaturas e ícones para as abas salvas"
|
||||||
|
thumbnail_capture_notice1: "Requer permissão para acessar o conteúdo dos sites visitados"
|
||||||
|
thumbnail_capture_notice2: "Desativar esse recurso pode melhorar o desempenho em coleções grandes"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Eliminar miniaturas guardadas"
|
||||||
|
title: "Excluir todas as miniaturas salvas?"
|
||||||
|
prompt: "Esta ação removerá todas as miniaturas, pré-visualizações e ícones salvos para suas abas salvas. Esta ação não pode ser desfeita."
|
||||||
about:
|
about:
|
||||||
title: "Sobre"
|
title: "Sobre"
|
||||||
developed_by: "Desenvolvido por Eugene Fox"
|
developed_by: "Desenvolvido por Eugene Fox"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "Meu site"
|
website: "Meu site"
|
||||||
source: "Código-fonte"
|
source: "Código-fonte"
|
||||||
changelog: "Registro de alterações"
|
changelog: "Registro de alterações"
|
||||||
|
privacy: "Política de Privacidade"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "Esta coleção está vazia"
|
empty: "Esta coleção está vazia"
|
||||||
|
|||||||
+22
-4
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!"
|
text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Читать блог"
|
visit_blog: "Читать блог"
|
||||||
|
settingsReview:
|
||||||
|
title: "Проверьте настройки"
|
||||||
|
action: "Все настройки"
|
||||||
|
analytics:
|
||||||
|
title: "Эта статистика позволит улучшать расширение"
|
||||||
|
p1: "Мы собираем только статистику использования (количество коллекций, используемые функции и т.д.)"
|
||||||
|
p2: "Мы не собираем ваши личные данные!"
|
||||||
|
p3_text: "Полный список собираемых данных можно посмотреть"
|
||||||
|
p3_link: "здесь"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Показывать счетчик"
|
show_badge: "Показывать счетчик"
|
||||||
show_notification: "Показывать уведомление при сохранении через контекстное меню"
|
show_notification: "Показывать уведомление при сохранении через контекстное меню"
|
||||||
unload_tabs: "Не загружать вкладки после открытия"
|
unload_tabs: "Не загружать вкладки после открытия"
|
||||||
|
allow_analytics: "Разрешить сбор анонимной статистики"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Открывать список коллекций в:"
|
title: "Открывать список коллекций в:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке."
|
text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке."
|
||||||
action: "Отключить и перезагрузить расширение"
|
action: "Отключить и перезагрузить расширение"
|
||||||
|
thumbnail_capture: "Сохранять превью и иконки для сохранённых вкладок"
|
||||||
|
thumbnail_capture_notice1: "Необходим доступ к содержанию посещенных веб-сайтов"
|
||||||
|
thumbnail_capture_notice2: "Отключение этой функции может улучшить производительность при большом количестве сохраненных вкладок"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Удалить сохранённые иконки"
|
||||||
|
title: "Удалить превью и иконки?"
|
||||||
|
prompt: "Это действие удалит все превью и иконки у ваших сохраненных вкладок. Это действие не может быть отменено."
|
||||||
about:
|
about:
|
||||||
title: "О расширении"
|
title: "О расширении"
|
||||||
developed_by: "Разработчик: Евгений Лис"
|
developed_by: "Разработчик: Евгений Лис"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "Мой веб-сайт"
|
website: "Мой веб-сайт"
|
||||||
source: "Исходный код"
|
source: "Исходный код"
|
||||||
changelog: "Список изменений"
|
changelog: "Список изменений"
|
||||||
|
privacy: "Политика конфиденциальности"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "Эта коллекция пуста"
|
empty: "Эта коллекция пуста"
|
||||||
@@ -149,13 +167,13 @@ collections:
|
|||||||
title: "Требуется разрешение"
|
title: "Требуется разрешение"
|
||||||
message:
|
message:
|
||||||
edge:
|
edge:
|
||||||
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме InPrivate"
|
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме InPrivate"
|
||||||
p2: "Для этого нажмите \"Настройки\" и затем отметьте опцию \"Разрешить в режиме InPrivate\""
|
p2: "Для этого нажмите \"Настройки\" и затем отметьте опцию \"Разрешить в режиме InPrivate\""
|
||||||
firefox:
|
firefox:
|
||||||
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в приватном окне"
|
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в приватном окне"
|
||||||
p2: "Для этого нажмите \"Настройки\", перейдите в \"Подробности\" и разрешите \"Запуск в приватных окнах\""
|
p2: "Для этого нажмите \"Настройки\", перейдите в \"Подробности\" и разрешите \"Запуск в приватных окнах\""
|
||||||
chrome:
|
chrome:
|
||||||
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме инкогнито"
|
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме инкогнито"
|
||||||
p2: "Для этого нажмите \"Настройки\" и отметьте опцию \"Разрешить использование в режиме инкогнито\""
|
p2: "Для этого нажмите \"Настройки\" и отметьте опцию \"Разрешить использование в режиме инкогнито\""
|
||||||
action: "Настройки"
|
action: "Настройки"
|
||||||
menu:
|
menu:
|
||||||
@@ -246,6 +264,6 @@ parse_error_message:
|
|||||||
|
|
||||||
merge_conflict_message:
|
merge_conflict_message:
|
||||||
title: "В локальном и облачном хранилищах есть конфликтующие изменения."
|
title: "В локальном и облачном хранилищах есть конфликтующие изменения."
|
||||||
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако, либо принять изменения из облака."
|
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако либо принять изменения из облака."
|
||||||
accept_local: "Заменить локальной"
|
accept_local: "Заменить локальной"
|
||||||
accept_cloud: "Принять облачные изменения"
|
accept_cloud: "Принять облачные изменения"
|
||||||
|
|||||||
+19
-1
@@ -36,6 +36,15 @@ features:
|
|||||||
text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!"
|
text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "Читати блог"
|
visit_blog: "Читати блог"
|
||||||
|
settingsReview:
|
||||||
|
title: "Перевірте налаштування"
|
||||||
|
action: "Всi налаштування"
|
||||||
|
analytics:
|
||||||
|
title: "Ця статистика дозволить покращувати розширення"
|
||||||
|
p1: "Ми збираємо лише статистику використання (кількість колекцій, використовувані функції тощо)"
|
||||||
|
p2: "Ми не збираємо ваші особисті дані!"
|
||||||
|
p3_text: "Повний список зібраних даних можна подивитися"
|
||||||
|
p3_link: "тут"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
@@ -74,6 +83,7 @@ options_page:
|
|||||||
show_badge: "Показувати лічильник"
|
show_badge: "Показувати лічильник"
|
||||||
show_notification: "Показувати сповіщення при збереженні через контекстне меню"
|
show_notification: "Показувати сповіщення при збереженні через контекстне меню"
|
||||||
unload_tabs: "Не завантажувати вкладки після відкриття"
|
unload_tabs: "Не завантажувати вкладки після відкриття"
|
||||||
|
allow_analytics: "Дозволити збір анонімної статистики"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "Відкривати список колекцій у:"
|
title: "Відкривати список колекцій у:"
|
||||||
options:
|
options:
|
||||||
@@ -121,6 +131,13 @@ options_page:
|
|||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
|
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
|
||||||
action: "Вимкнути та перезавантажити розширення"
|
action: "Вимкнути та перезавантажити розширення"
|
||||||
|
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок"
|
||||||
|
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
|
||||||
|
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "Видалити збережені іконки"
|
||||||
|
title: "Видалити превью і іконки?"
|
||||||
|
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
|
||||||
about:
|
about:
|
||||||
title: "О розширенні"
|
title: "О розширенні"
|
||||||
developed_by: "Розробник: Євген Лис"
|
developed_by: "Розробник: Євген Лис"
|
||||||
@@ -133,6 +150,7 @@ options_page:
|
|||||||
website: "Мій веб-сайт"
|
website: "Мій веб-сайт"
|
||||||
source: "Вихідний код"
|
source: "Вихідний код"
|
||||||
changelog: "Список змін"
|
changelog: "Список змін"
|
||||||
|
privacy: "Політика конфіденційності"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "Ця колекція пуста"
|
empty: "Ця колекція пуста"
|
||||||
@@ -246,6 +264,6 @@ parse_error_message:
|
|||||||
|
|
||||||
merge_conflict_message:
|
merge_conflict_message:
|
||||||
title: "В локальному і облачному хранилищах є конфліктуючі зміни."
|
title: "В локальному і облачному хранилищах є конфліктуючі зміни."
|
||||||
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі, або прийняти зміни з хмари."
|
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі або прийняти зміни з хмари."
|
||||||
accept_local: "Заменить локальною"
|
accept_local: "Заменить локальною"
|
||||||
accept_cloud: "Прийняти облачні зміни"
|
accept_cloud: "Прийняти облачні зміни"
|
||||||
|
|||||||
+66
-48
@@ -5,8 +5,8 @@ manifest:
|
|||||||
|
|
||||||
shortcuts:
|
shortcuts:
|
||||||
toggle_sidebar: "打开收藏列表"
|
toggle_sidebar: "打开收藏列表"
|
||||||
set_aside: "将标签放到一边"
|
set_aside: "搁置标签页"
|
||||||
save_tabs: "保存标签而不关闭"
|
save_tabs: "保存标签页但不关闭"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
actions:
|
actions:
|
||||||
@@ -14,10 +14,10 @@ common:
|
|||||||
save: "保存"
|
save: "保存"
|
||||||
close: "关闭"
|
close: "关闭"
|
||||||
delete: "删除"
|
delete: "删除"
|
||||||
reset_filters: "重置筛选器"
|
reset_filters: "重置筛选"
|
||||||
cta:
|
cta:
|
||||||
feedback: "留下反馈"
|
feedback: "留下反馈"
|
||||||
sponsor: "请我喝咖啡"
|
sponsor: "请我喝杯咖啡!"
|
||||||
tooltips:
|
tooltips:
|
||||||
more: "更多"
|
more: "更多"
|
||||||
delete_prompt: "您确定吗?此操作无法撤销。"
|
delete_prompt: "您确定吗?此操作无法撤销。"
|
||||||
@@ -25,42 +25,51 @@ common:
|
|||||||
features:
|
features:
|
||||||
v3welcome:
|
v3welcome:
|
||||||
title: "欢迎使用搁置的标签页 3.0"
|
title: "欢迎使用搁置的标签页 3.0"
|
||||||
text1: "我们很高兴宣布搁置的标签页扩展的新重大更新!"
|
text1: "我们很高兴宣布搁置的标签页扩展新的重大更新!"
|
||||||
text2: "此更新带来了全新的用户界面,以及许多新功能,包括:"
|
text2: "此更新带来了全新的用户界面,以及许多新功能,包括:"
|
||||||
list:
|
list:
|
||||||
item1: "支持标签组"
|
item1: "支持标签组"
|
||||||
item2: "收藏自定义"
|
item2: "收藏自定义"
|
||||||
item3: "拖放重新排序和组织"
|
item3: "拖放排序和整理"
|
||||||
item4: "从头开始手动创建收藏"
|
item4: "从零开始创建收藏"
|
||||||
item5: "以及更多!"
|
item5: "以及更多!"
|
||||||
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
|
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
|
||||||
actions:
|
actions:
|
||||||
visit_blog: "阅读开发博客"
|
visit_blog: "阅读开发博客"
|
||||||
|
settingsReview:
|
||||||
|
title: "检查您的设置"
|
||||||
|
action: "所有设置"
|
||||||
|
analytics:
|
||||||
|
title: "这些统计数据将帮助我们改进扩展"
|
||||||
|
p1: "我们只收集使用统计数据(收藏数量、使用的功能等)"
|
||||||
|
p2: "我们不会收集您的任何数据!"
|
||||||
|
p3_text: "请参阅我们收集内容的"
|
||||||
|
p3_link: "完整列表"
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
tabs_saved:
|
tabs_saved:
|
||||||
title: "新收藏已创建"
|
title: "已创建新收藏"
|
||||||
message: "您的标签已保存到新收藏中"
|
message: "您的标签页已保存到新收藏中"
|
||||||
error_quota_exceeded:
|
error_quota_exceeded:
|
||||||
title: "超出最大云写入操作"
|
title: "超出最大云储存写入操作"
|
||||||
message: "我们已将您的标签保存到本地存储。您需要手动更新云存储"
|
message: "我们已将您的标签页保存到本地存储。您需要手动更新云存储"
|
||||||
error_storage_full:
|
error_storage_full:
|
||||||
title: "您的云存储已满"
|
title: "您的云存储已满"
|
||||||
message: "我们已将您的标签保存到本地存储。请清理一些云存储空间"
|
message: "我们已将您的标签页保存到本地存储。请清理一些云存储空间"
|
||||||
bookmark_saved:
|
bookmark_saved:
|
||||||
title: "已导出到书签"
|
title: "已导出到书签"
|
||||||
message: "您的收藏已导出到书签"
|
message: "您的收藏已导出到书签"
|
||||||
partial_save:
|
partial_save:
|
||||||
title: "某些标签无法保存"
|
title: "部分标签页无法保存"
|
||||||
message: "某些标签是我们无法访问的系统标签。它们已被跳过"
|
message: "部分标签页是无法访问的系统标签页。它们已被跳过"
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
save:
|
save:
|
||||||
all: "保存所有标签"
|
all: "保存所有标签页"
|
||||||
selected: "保存选定的标签"
|
selected: "保存选定的标签页"
|
||||||
set_aside:
|
set_aside:
|
||||||
all: "将所有标签放到一边"
|
all: "搁置所有标签页"
|
||||||
selected: "将选定的标签放到一边"
|
selected: "搁置选定的标签页"
|
||||||
show_collections: "显示收藏"
|
show_collections: "显示收藏"
|
||||||
|
|
||||||
options_page:
|
options_page:
|
||||||
@@ -69,11 +78,12 @@ options_page:
|
|||||||
title: "常规"
|
title: "常规"
|
||||||
options:
|
options:
|
||||||
always_show_toolbars: "始终显示工具栏"
|
always_show_toolbars: "始终显示工具栏"
|
||||||
include_pinned: "保存所有标签时包括固定标签"
|
include_pinned: "保存所有标签页时包括已固定的标签页"
|
||||||
show_delete_prompt: "删除项目时要求确认"
|
show_delete_prompt: "删除项目时要求确认"
|
||||||
show_badge: "显示计数徽章"
|
show_badge: "显示计数角标"
|
||||||
show_notification: "使用上下文菜单保存标签时显示通知"
|
show_notification: "使用上下文菜单保存标签页时显示通知"
|
||||||
unload_tabs: "打开后不加载标签"
|
unload_tabs: "打开后不加载标签页"
|
||||||
|
allow_analytics: "允许收集匿名统计数据"
|
||||||
list_locations:
|
list_locations:
|
||||||
title: "在以下位置打开收藏列表:"
|
title: "在以下位置打开收藏列表:"
|
||||||
options:
|
options:
|
||||||
@@ -92,15 +102,15 @@ options_page:
|
|||||||
title: "默认操作"
|
title: "默认操作"
|
||||||
options:
|
options:
|
||||||
save_actions:
|
save_actions:
|
||||||
title: "保存标签时的默认操作"
|
title: "保存标签页时的默认操作"
|
||||||
options:
|
options:
|
||||||
set_aside: "保存并关闭标签"
|
set_aside: "保存并关闭标签页"
|
||||||
save: "保存标签而不关闭"
|
save: "保存标签页而不关闭"
|
||||||
restore_actions:
|
restore_actions:
|
||||||
title: "打开收藏时的默认操作"
|
title: "打开收藏时的默认操作"
|
||||||
options:
|
options:
|
||||||
open: "仅打开标签"
|
open: "仅打开标签页"
|
||||||
restore: "打开标签并删除收藏"
|
restore: "打开标签页并删除收藏"
|
||||||
storage:
|
storage:
|
||||||
title: "存储"
|
title: "存储"
|
||||||
capacity:
|
capacity:
|
||||||
@@ -114,29 +124,37 @@ options_page:
|
|||||||
import_prompt:
|
import_prompt:
|
||||||
title: "导入数据"
|
title: "导入数据"
|
||||||
warning_title: "这是不可逆的操作"
|
warning_title: "这是不可逆的操作"
|
||||||
warning_text: "这将覆盖您的所有数据。请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
|
warning_text: "这将覆盖您的所有数据!请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
|
||||||
proceed: "选择文件"
|
proceed: "选择文件"
|
||||||
enable: "启用云存储"
|
enable: "启用云存储"
|
||||||
disable: "禁用云存储"
|
disable: "禁用云存储"
|
||||||
disable_prompt:
|
disable_prompt:
|
||||||
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
|
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
|
||||||
action: "禁用并重新加载扩展"
|
action: "禁用并重新加载扩展"
|
||||||
|
thumbnail_capture: "为已保存的标签页保存缩略图和图标"
|
||||||
|
thumbnail_capture_notice1: "需要访问已访问网站内容的权限"
|
||||||
|
thumbnail_capture_notice2: "有大量收藏时,禁用此功能可能会提高性能"
|
||||||
|
clear_thumbnails:
|
||||||
|
action: "删除已保存的图标"
|
||||||
|
title: "删除缩略图和图标?"
|
||||||
|
prompt: "此操作将删除您已保存标签页的所有缩略图和图标。此操作无法撤消。"
|
||||||
about:
|
about:
|
||||||
title: "关于"
|
title: "关于"
|
||||||
developed_by: "由尤金·福克斯开发"
|
developed_by: "由尤金·福克斯开发"
|
||||||
licensed_under: "许可协议"
|
licensed_under: "许可协议"
|
||||||
mit_license: "MIT 许可协议"
|
mit_license: "MIT 协议"
|
||||||
translation_cta:
|
translation_cta:
|
||||||
text: "发现错别字或想为您的语言提供翻译?"
|
text: "发现错别字或想为您的语言提供翻译?"
|
||||||
button: "从这里开始"
|
button: "快速入门"
|
||||||
links:
|
links:
|
||||||
website: "我的网站"
|
website: "我的网站"
|
||||||
source: "源代码"
|
source: "源代码"
|
||||||
changelog: "更新日志"
|
changelog: "更新日志"
|
||||||
|
privacy: "隐私政策"
|
||||||
|
|
||||||
collections:
|
collections:
|
||||||
empty: "此收藏为空"
|
empty: "此收藏为空"
|
||||||
tabs_count: "$1 个标签"
|
tabs_count: "$1 个标签页"
|
||||||
actions:
|
actions:
|
||||||
open: "打开所有"
|
open: "打开所有"
|
||||||
restore: "恢复所有"
|
restore: "恢复所有"
|
||||||
@@ -152,35 +170,35 @@ collections:
|
|||||||
p1: "扩展需要权限才能在 InPrivate 窗口中打开标签"
|
p1: "扩展需要权限才能在 InPrivate 窗口中打开标签"
|
||||||
p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项"
|
p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项"
|
||||||
firefox:
|
firefox:
|
||||||
p1: "扩展需要权限才能在隐私窗口中打开标签"
|
p1: "扩展需要权限才能在隐私窗口中打开标签页"
|
||||||
p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”"
|
p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”"
|
||||||
chrome:
|
chrome:
|
||||||
p1: "扩展需要权限才能在隐身窗口中打开标签"
|
p1: "扩展需要权限才能在隐身窗口中打开标签页"
|
||||||
p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项"
|
p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项"
|
||||||
action: "设置"
|
action: "设置"
|
||||||
menu:
|
menu:
|
||||||
delete: "删除收藏"
|
delete: "删除收藏"
|
||||||
add_selected: "添加选定的标签"
|
add_selected: "添加选定的标签页"
|
||||||
add_all: "添加所有标签"
|
add_all: "添加所有标签页"
|
||||||
add_group: "添加空组"
|
add_group: "添加空分组"
|
||||||
export_bookmarks: "导出到书签"
|
export_bookmarks: "导出到书签"
|
||||||
edit: "编辑收藏"
|
edit: "编辑收藏"
|
||||||
|
|
||||||
groups:
|
groups:
|
||||||
title: "组"
|
title: "分组"
|
||||||
pinned: "已固定"
|
pinned: "已固定"
|
||||||
open: "打开所有"
|
open: "打开所有"
|
||||||
empty: "此组为空"
|
empty: "此分组为空"
|
||||||
menu:
|
menu:
|
||||||
new_window: "在新窗口中打开"
|
new_window: "在新窗口中打开"
|
||||||
add_selected: "添加选定的标签"
|
add_selected: "添加选定的标签页"
|
||||||
add_all: "添加所有标签"
|
add_all: "添加所有标签页"
|
||||||
edit: "编辑组"
|
edit: "编辑分组"
|
||||||
ungroup: "取消分组"
|
ungroup: "取消分组"
|
||||||
delete: "删除组"
|
delete: "删除分组"
|
||||||
|
|
||||||
tabs:
|
tabs:
|
||||||
delete: "删除标签"
|
delete: "删除标签页"
|
||||||
|
|
||||||
colors:
|
colors:
|
||||||
none: "无颜色"
|
none: "无颜色"
|
||||||
@@ -199,8 +217,8 @@ dialogs:
|
|||||||
edit:
|
edit:
|
||||||
title:
|
title:
|
||||||
edit_collection: "编辑收藏"
|
edit_collection: "编辑收藏"
|
||||||
edit_group: "编辑组"
|
edit_group: "编辑分组"
|
||||||
new_group: "新组"
|
new_group: "新分组"
|
||||||
new_collection: "新收藏"
|
new_collection: "新收藏"
|
||||||
collection_title: "标题"
|
collection_title: "标题"
|
||||||
color: "颜色"
|
color: "颜色"
|
||||||
@@ -210,7 +228,7 @@ main:
|
|||||||
create_collection: "创建新收藏"
|
create_collection: "创建新收藏"
|
||||||
menu:
|
menu:
|
||||||
tiles_view: "平铺视图"
|
tiles_view: "平铺视图"
|
||||||
changelog: "更新内容?"
|
changelog: "更新内容"
|
||||||
list:
|
list:
|
||||||
searchbar:
|
searchbar:
|
||||||
title: "搜索"
|
title: "搜索"
|
||||||
@@ -225,7 +243,7 @@ main:
|
|||||||
custom: "自定义"
|
custom: "自定义"
|
||||||
empty:
|
empty:
|
||||||
title: "这里还没有内容"
|
title: "这里还没有内容"
|
||||||
message: "将当前标签放到一边,或创建新收藏"
|
message: "搁置当前标签页,或创建新收藏"
|
||||||
empty_search:
|
empty_search:
|
||||||
title: "未找到任何内容"
|
title: "未找到任何内容"
|
||||||
message: "尝试更改搜索查询"
|
message: "尝试更改搜索查询"
|
||||||
@@ -247,5 +265,5 @@ parse_error_message:
|
|||||||
merge_conflict_message:
|
merge_conflict_message:
|
||||||
title: "您的本地和云存储有冲突的更改。"
|
title: "您的本地和云存储有冲突的更改。"
|
||||||
message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。"
|
message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。"
|
||||||
accept_local: "用本地替换"
|
accept_local: "采用本地替换云端"
|
||||||
accept_cloud: "接受云端更改"
|
accept_cloud: "接受云端更改"
|
||||||
|
|||||||
+24
-25
@@ -1,47 +1,46 @@
|
|||||||
{
|
{
|
||||||
"name": "tabs-aside",
|
"name": "tabs-aside",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.0.0-rc6",
|
"version": "3.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
"build": "wxt build --mv3",
|
"build": "yarn lint && wxt build --mv3",
|
||||||
"zip": "wxt zip --mv3",
|
"zip": "yarn lint && wxt zip --mv3",
|
||||||
"lint": "tsc --noEmit && eslint . -c eslint.config.js",
|
"lint": "tsc --noEmit && eslint . -c eslint.config.js",
|
||||||
"prebuild": "yarn lint",
|
"prepare": "wxt prepare",
|
||||||
"prezip": "yarn lint",
|
"postinstall": "yarn prepare"
|
||||||
"postinstall": "wxt prepare"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@fluentui/react-components": "^9.63.0",
|
"@fluentui/react-components": "^9.72.0",
|
||||||
"@fluentui/react-icons": "^2.0.298",
|
"@fluentui/react-icons": "^2.0.311",
|
||||||
"@webext-core/messaging": "^2.2.0",
|
"@webext-core/messaging": "^2.3.0",
|
||||||
"@wxt-dev/analytics": "^0.4.1",
|
"@wxt-dev/analytics": "^0.4.1",
|
||||||
"@wxt-dev/i18n": "^0.2.3",
|
"@wxt-dev/i18n": "^0.2.4",
|
||||||
"lzutf8": "^0.6.3",
|
"lzutf8": "^0.6.3",
|
||||||
"react": "^18.3.1",
|
"react": "~19.2.0",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "~19.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/css": "^0.7.0",
|
"@eslint/css": "^0.11.1",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.37.0",
|
||||||
"@eslint/json": "^0.12.0",
|
"@eslint/json": "^0.13.2",
|
||||||
"@stylistic/eslint-plugin": "^4.2.0",
|
"@stylistic/eslint-plugin": "^5.4.0",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "~19.2.0",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "~19.2.0",
|
||||||
"@wxt-dev/module-react": "^1.1.3",
|
"@wxt-dev/module-react": "^1.1.5",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.4.0",
|
||||||
"scheduler": "0.23.0",
|
"scheduler": "0.23.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.3",
|
||||||
"typescript-eslint": "^8.31.1",
|
"typescript-eslint": "^8.45.0",
|
||||||
"vite": "^6.3.4",
|
"vite": "^7.1.9",
|
||||||
"wxt": "~0.19.29"
|
"wxt": "~0.19.29"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
"packageManager": "yarn@4.9.2"
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-13
@@ -31,19 +31,17 @@ Stemming its roots from the original Microsoft Edge browser feature, this extens
|
|||||||
|
|
||||||
It allows you to save and manage your tabs in a convenient way, providing a range of features that make it easy to organize and access your saved tabs.
|
It allows you to save and manage your tabs in a convenient way, providing a range of features that make it easy to organize and access your saved tabs.
|
||||||
|
|
||||||
<b>Features</b>
|
**Features**
|
||||||
<ul>
|
- **Save tabs:** Save all your open tabs in a single click, and restore them later
|
||||||
<li><b>Save tabs</b>: Save all your open tabs in a single click, and restore them later</li>
|
- **Organize tabs:** Create collections and subgroups to organize your saved tabs
|
||||||
<li><b>Organize tabs</b>: Create collections and subgroups to organize your saved tabs</li>
|
- **Search tabs:** Quickly find the tabs you need using the search feature
|
||||||
<li><b>Search tabs</b>: Quickly find the tabs you need using the search feature</li>
|
- **Sync across devices:** Access your saved tabs from any device with your account
|
||||||
<li><b>Sync across devices</b>: Access your saved tabs from any device with your account</li>
|
- **Go dark:** Dark mode support for a more comfortable browsing experience
|
||||||
<li><b>Go dark</b>: Dark mode support for a more comfortable browsing experience</li>
|
- **Personalize:** Change the appearance and behavior of the extension to suit your needs
|
||||||
<li><b>Personalize</b>: Change the appearance and behavior of the extension to suit your needs</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
<b>Hey, it's an open-source software!</b>
|
**Hey, it's an open-source software!**
|
||||||
If you know how to improve this extension you can check <a href="https://github.com/xfox111/TabsAsideExtension">its GitHub Repository</a>
|
If you know how to improve this extension you can check [its GitHub Repository](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
Check out <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">release changelog</a>
|
Check out [release changelog](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
+11
-13
@@ -30,19 +30,17 @@ Basada en la funcionalidad original del navegador Microsoft Edge, esta extensió
|
|||||||
|
|
||||||
Te permite guardar y gestionar tus pestañas de manera conveniente, proporcionando una gama de características que facilitan organizar y acceder a tus pestañas guardadas.
|
Te permite guardar y gestionar tus pestañas de manera conveniente, proporcionando una gama de características que facilitan organizar y acceder a tus pestañas guardadas.
|
||||||
|
|
||||||
<b>Características</b>
|
**Características**
|
||||||
<ul>
|
- **Guardar pestañas:** Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde
|
||||||
<li><b>Guardar pestañas</b>: Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde</li>
|
- **Organizar pestañas:** Crea colecciones y subgrupos para organizar tus pestañas guardadas
|
||||||
<li><b>Organizar pestañas</b>: Crea colecciones y subgrupos para organizar tus pestañas guardadas</li>
|
- **Buscar pestañas:** Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda
|
||||||
<li><b>Buscar pestañas</b>: Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda</li>
|
- **Sincronizar entre dispositivos:** Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta
|
||||||
<li><b>Sincronizar entre dispositivos</b>: Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta</li>
|
- **Modo oscuro:** Soporte para modo oscuro para una experiencia de navegación más cómoda
|
||||||
<li><b>Modo oscuro</b>: Soporte para modo oscuro para una experiencia de navegación más cómoda</li>
|
- **Personalizar:** Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades
|
||||||
<li><b>Personalizar</b>: Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
<b>¡Oye, es un software de código abierto!</b>
|
**¡Oye, es un software de código abierto!**
|
||||||
Si sabes cómo mejorar esta extensión, puedes revisar <a href="https://github.com/xfox111/TabsAsideExtension">su repositorio de GitHub</a>
|
Si sabes cómo mejorar esta extensión, puedes revisar [su repositorio de GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
Consulta el <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">registro de cambios</a>
|
Consulta el [registro de cambios](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
+11
-13
@@ -30,19 +30,17 @@ Radicata nella funzionalità originale del browser Microsoft Edge, questa estens
|
|||||||
|
|
||||||
Ti consente di salvare e gestire le tue schede in modo conveniente, fornendo una gamma di funzionalità che rendono facile organizzare e accedere alle tue schede salvate.
|
Ti consente di salvare e gestire le tue schede in modo conveniente, fornendo una gamma di funzionalità che rendono facile organizzare e accedere alle tue schede salvate.
|
||||||
|
|
||||||
<b>Funzionalità</b>
|
**Funzionalità**
|
||||||
<ul>
|
- **Salva schede:** Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito
|
||||||
<li><b>Salva schede</b>: Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito</li>
|
- **Organizza schede:** Crea collezioni e sottogruppi per organizzare le tue schede salvate
|
||||||
<li><b>Organizza schede</b>: Crea collezioni e sottogruppi per organizzare le tue schede salvate</li>
|
- **Cerca schede:** Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca
|
||||||
<li><b>Cerca schede</b>: Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca</li>
|
- **Sincronizza tra dispositivi:** Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account
|
||||||
<li><b>Sincronizza tra dispositivi</b>: Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account</li>
|
- **Modalità scura:** Supporto per la modalità scura per un'esperienza di navigazione più confortevole
|
||||||
<li><b>Modalità scura</b>: Supporto per la modalità scura per un'esperienza di navigazione più confortevole</li>
|
- **Personalizza:** Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze
|
||||||
<li><b>Personalizza</b>: Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
<b>Ehi, è un software open-source!</b>
|
**Ehi, è un software open-source!**
|
||||||
Se sai come migliorare questa estensione, puoi controllare <a href="https://github.com/xfox111/TabsAsideExtension">il suo repository GitHub</a>
|
Se sai come migliorare questa estensione, puoi controllare [il suo repository GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
Consulta il <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">registro delle modifiche</a>
|
Consulta il [registro delle modifiche](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
+11
-13
@@ -30,19 +30,17 @@ Zainspirowane funkcją z pierwszych wersji Microsoft Edge, to rozszerzenie stał
|
|||||||
|
|
||||||
Pozwala wygodnie zapisywać i zarządzać kartami, oferując wiele funkcji, które ułatwiają organizację i dostęp do zapisanych kart.
|
Pozwala wygodnie zapisywać i zarządzać kartami, oferując wiele funkcji, które ułatwiają organizację i dostęp do zapisanych kart.
|
||||||
|
|
||||||
<b>Funkcje</b>
|
**Funkcje**
|
||||||
<ul>
|
- **Zapisywanie kart:** Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później
|
||||||
<li><b>Zapisywanie kart</b>: Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później</li>
|
- **Organizacja kart:** Twórz kolekcje i podgrupy, aby organizować zapisane karty
|
||||||
<li><b>Organizacja kart</b>: Twórz kolekcje i podgrupy, aby organizować zapisane karty</li>
|
- **Wyszukiwanie kart:** Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania
|
||||||
<li><b>Wyszukiwanie kart</b>: Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania</li>
|
- **Synchronizacja między urządzeniami:** Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta
|
||||||
<li><b>Synchronizacja między urządzeniami</b>: Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta</li>
|
- **Tryb ciemny:** Obsługa trybu ciemnego dla bardziej komfortowego użytkowania
|
||||||
<li><b>Tryb ciemny</b>: Obsługa trybu ciemnego dla bardziej komfortowego użytkowania</li>
|
- **Personalizacja:** Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb
|
||||||
<li><b>Personalizacja</b>: Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
<b>Przy okazji, to rozszerzenie open-source!</b>
|
**Przy okazji, to rozszerzenie open-source!**
|
||||||
Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić <a href="https://github.com/xfox111/TabsAsideExtension">jego repozytorium na GitHubie</a>
|
Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić [jego repozytorium na GitHubie](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
<a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">Lista zmian w najnowszej wersji</a>
|
[Lista zmian w najnowszej wersji](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
@@ -31,19 +31,17 @@ Originando-se do recurso original do navegador Microsoft Edge, esta extensão cr
|
|||||||
|
|
||||||
Ela permite que você salve e gerencie suas abas de forma conveniente, oferecendo uma variedade de recursos que facilitam a organização e o acesso às abas salvas.
|
Ela permite que você salve e gerencie suas abas de forma conveniente, oferecendo uma variedade de recursos que facilitam a organização e o acesso às abas salvas.
|
||||||
|
|
||||||
<b>Recursos</b>
|
**Recursos**
|
||||||
<ul>
|
- **Salvar abas:** Salve todas as suas abas abertas com um único clique e restaure-as depois
|
||||||
<li><b>Salvar abas</b>: Salve todas as suas abas abertas com um único clique e restaure-as depois</li>
|
- **Organizar abas:** Crie coleções e subgrupos para organizar suas abas salvas
|
||||||
<li><b>Organizar abas</b>: Crie coleções e subgrupos para organizar suas abas salvas</li>
|
- **Pesquisar abas:** Encontre rapidamente as abas que você precisa usando o recurso de pesquisa
|
||||||
<li><b>Pesquisar abas</b>: Encontre rapidamente as abas que você precisa usando o recurso de pesquisa</li>
|
- **Sincronizar entre dispositivos:** Acesse suas abas salvas de qualquer dispositivo com sua conta
|
||||||
<li><b>Sincronizar entre dispositivos</b>: Acesse suas abas salvas de qualquer dispositivo com sua conta</li>
|
- **Modo escuro:** Suporte ao modo escuro para uma experiência de navegação mais confortável
|
||||||
<li><b>Modo escuro</b>: Suporte ao modo escuro para uma experiência de navegação mais confortável</li>
|
- **Personalizar:** Altere a aparência e o comportamento da extensão conforme suas necessidades
|
||||||
<li><b>Personalizar</b>: Altere a aparência e o comportamento da extensão conforme suas necessidades</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
|
||||||
<b>Ei, é um software de código aberto!</b>
|
**Ei, é um software de código aberto!**
|
||||||
Se você sabe como melhorar esta extensão, confira <a href="https://github.com/xfox111/TabsAsideExtension">seu repositório no GitHub</a>
|
Se você sabe como melhorar esta extensão, confira [seu repositório no GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
Veja o <a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">changelog das versões</a>
|
Veja o [changelog das versões](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
+11
-13
@@ -30,19 +30,17 @@ 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>
|
|
||||||
|
|
||||||
Посетите <a href="https://at.xfox111.net/tabs-aside-3-0">наш блог</a>, чтобы узнать больше о всех новых функциях и улучшениях в Отложенных вкладках 3.0 по ссылке
|
Посетите [наш блог](https://at.xfox111.net/tabs-aside-3-0), чтобы узнать больше о всех новых функциях и улучшениях в Отложенных вкладках 3.0 по ссылке
|
||||||
|
|
||||||
<b>Кстати это опенсорс расширение!</b>
|
**Кстати это опенсорс расширение!**
|
||||||
Если вы знаете, как можно его улучшить, можете перейти на <a href="https://github.com/xfox111/TabsAsideExtension">страницу GitHub репозитория проекта</a>
|
Если вы знаете, как можно его улучшить, можете перейти на [страницу GitHub репозитория проекта](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
<a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">Список изменений последней версии</a>
|
[Список изменений последней версии](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
+11
-13
@@ -30,19 +30,17 @@ 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>
|
|
||||||
|
|
||||||
Відвідайте <a href="https://at.xfox111.net/tabs-aside-3-0">наш блог</a>, щоб дізнатися більше про всі нові функції та покращення у Відкладених вкладках 3.0
|
Відвідайте [наш блог](https://at.xfox111.net/tabs-aside-3-0), щоб дізнатися більше про всі нові функції та покращення у Відкладених вкладках 3.0
|
||||||
|
|
||||||
<b>До речі, це опенсорс розширення!</b>
|
**До речі, це опенсорс розширення!**
|
||||||
Якщо ви знаєте, як покращити це розширення, ви можете відвідати <a href="https://github.com/xfox111/TabsAsideExtension">його репозиторій на GitHub</a>
|
Якщо ви знаєте, як покращити це розширення, ви можете відвідати [його репозиторій на GitHub](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
<a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">Список змін останньої версії</a>
|
[Список змін останньої версії](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
@@ -30,19 +30,17 @@ 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>
|
|
||||||
|
|
||||||
查看我们关于 <a href="https://at.xfox111.net/tabs-aside-3-0">搁置的标签页 3.0</a> 的所有新功能和改进的<a href="https://at.xfox111.net/tabs-aside-3-0">博客文章</a>
|
查看我们关于 搁置的标签页 3.0 的所有新功能和改进的[博客文章](https://at.xfox111.net/tabs-aside-3-0)
|
||||||
|
|
||||||
<b>嘿,这是一个开源软件!</b>
|
**嘿,这是一个开源软件!**
|
||||||
如果您知道如何改进此扩展,可以查看<a href="https://github.com/xfox111/TabsAsideExtension">其 GitHub 仓库</a>
|
如果您知道如何改进此扩展,可以查看[其 GitHub 仓库](https://github.com/xfox111/TabsAsideExtension)
|
||||||
|
|
||||||
查看<a href="https://github.com/xfox111/TabsAsideExtension/releases/latest">发布更新日志</a>
|
查看[发布更新日志](https://github.com/xfox111/TabsAsideExtension/releases/latest)
|
||||||
|
|||||||
+12
-5
@@ -1,26 +1,33 @@
|
|||||||
import { trackError } from "@/features/analytics";
|
import { trackError } from "@/features/analytics";
|
||||||
import { GraphicsStorage } from "@/models/CollectionModels";
|
import { CollectionItem, GraphicsStorage, GroupItem } from "@/models/CollectionModels";
|
||||||
import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger } from "@webext-core/messaging";
|
import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger, ExtensionSendMessageArgs, GetDataType, GetReturnType } from "@webext-core/messaging";
|
||||||
|
|
||||||
type ProtocolMap =
|
type ProtocolMap =
|
||||||
{
|
{
|
||||||
addThumbnail(data: { url: string; thumbnail: string; }): void;
|
addThumbnail(data: { url: string; thumbnail: string; }): void;
|
||||||
getGraphicsCache(): GraphicsStorage;
|
getGraphicsCache(): GraphicsStorage;
|
||||||
refreshCollections(): void;
|
refreshCollections(): void;
|
||||||
|
|
||||||
|
openCollection(data: { collection: CollectionItem; targetWindow: "new" | "incognito"; }): void;
|
||||||
|
openGroup(data: { group: GroupItem; newWindow: boolean; }): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap>
|
function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap>
|
||||||
{
|
{
|
||||||
const { onMessage, sendMessage, removeAllListeners } = defineExtensionMessaging<ProtocolMap>(config);
|
const { onMessage, sendMessage, removeAllListeners }: ExtensionMessenger<ProtocolMap> = defineExtensionMessaging<ProtocolMap>(config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onMessage,
|
onMessage,
|
||||||
removeAllListeners,
|
removeAllListeners,
|
||||||
sendMessage: async (type, data, args): Promise<any> =>
|
async sendMessage<TType extends keyof ProtocolMap>(
|
||||||
|
type: TType,
|
||||||
|
data: GetDataType<ProtocolMap[TType]>,
|
||||||
|
...args: ExtensionSendMessageArgs
|
||||||
|
): Promise<GetReturnType<ProtocolMap[TType]>>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await sendMessage(type, data, args);
|
return await sendMessage(type, data, ...args);
|
||||||
}
|
}
|
||||||
catch (ex)
|
catch (ex)
|
||||||
{
|
{
|
||||||
|
|||||||
+15
-4
@@ -2,7 +2,7 @@ import { ConfigEnv, defineConfig, UserManifest } from "wxt";
|
|||||||
|
|
||||||
// See https://wxt.dev/api/config.html
|
// See https://wxt.dev/api/config.html
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module", "@wxt-dev/analytics/module"],
|
modules: ["@wxt-dev/module-react", "@wxt-dev/i18n/module"],
|
||||||
vite: () => ({
|
vite: () => ({
|
||||||
build:
|
build:
|
||||||
{
|
{
|
||||||
@@ -37,10 +37,15 @@ export default defineConfig({
|
|||||||
"tabs",
|
"tabs",
|
||||||
"notifications",
|
"notifications",
|
||||||
"contextMenus",
|
"contextMenus",
|
||||||
"bookmarks",
|
|
||||||
"tabGroups"
|
"tabGroups"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
optional_permissions:
|
||||||
|
[
|
||||||
|
"bookmarks",
|
||||||
|
"scripting"
|
||||||
|
],
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
{
|
{
|
||||||
show_collections:
|
show_collections:
|
||||||
@@ -71,7 +76,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
host_permissions: ["<all_urls>"]
|
optional_host_permissions: ["<all_urls>"]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (browser === "firefox")
|
if (browser === "firefox")
|
||||||
@@ -80,7 +85,13 @@ export default defineConfig({
|
|||||||
gecko:
|
gecko:
|
||||||
{
|
{
|
||||||
id: "tabsaside@xfox111.net",
|
id: "tabsaside@xfox111.net",
|
||||||
strict_min_version: "139.0"
|
strict_min_version: "139.0",
|
||||||
|
|
||||||
|
// @ts-expect-error Introduced in Firefox 139
|
||||||
|
data_collection_permissions: {
|
||||||
|
required: ["browsingActivity"],
|
||||||
|
optional: ["technicalAndInteraction"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user