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

Compare commits

..

10 Commits

Author SHA1 Message Date
xfox111 b51dd6083f chore(deps): WXT 0.20.0 bump + lockfile regen (#199)
* chore(deps): wxt 0.20.0 bump #134

* chore: 3.2.1 manifest bump
2025-11-14 02:16:57 +03:00
xfox111 3cd3c4453d feat: Minor 3.2.0 (#197)
* chore(loc): zh_CN translation improvements (#153)

* chore(deps): Bump typescript-eslint from 8.43.0 to 8.45.0 (#166)

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.43.0 to 8.45.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.45.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump @wxt-dev/analytics from 0.4.1 to 0.5.1 (#164)

Bumps [@wxt-dev/analytics](https://github.com/wxt-dev/wxt/tree/HEAD/packages/analytics) from 0.4.1 to 0.5.1.
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/analytics/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/v0.5.1/packages/analytics)

---
updated-dependencies:
- dependency-name: "@wxt-dev/analytics"
  dependency-version: 0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump eslint from 9.35.0 to 9.36.0 (#163)

Bumps [eslint](https://github.com/eslint/eslint) from 9.35.0 to 9.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.36.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump globals from 16.3.0 to 16.4.0 (#158)

Bumps [globals](https://github.com/sindresorhus/globals) from 16.3.0 to 16.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.3.0...v16.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump @eslint/css from 0.11.0 to 0.11.1 (#157)

Bumps [@eslint/css](https://github.com/eslint/css) from 0.11.0 to 0.11.1.
- [Release notes](https://github.com/eslint/css/releases)
- [Changelog](https://github.com/eslint/css/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/css/compare/css-v0.11.0...css-v0.11.1)

---
updated-dependencies:
- dependency-name: "@eslint/css"
  dependency-version: 0.11.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump @fluentui/react-icons from 2.0.309 to 2.0.311 (#156)

Bumps [@fluentui/react-icons](https://github.com/microsoft/fluentui-system-icons) from 2.0.309 to 2.0.311.
- [Changelog](https://github.com/microsoft/fluentui-system-icons/blob/main/fluentui-android-system-icons-release.yml)
- [Commits](https://github.com/microsoft/fluentui-system-icons/commits)

---
updated-dependencies:
- dependency-name: "@fluentui/react-icons"
  dependency-version: 2.0.311
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump typescript from 5.9.2 to 5.9.3 (#165)

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump vite from 7.1.5 to 7.1.7 (#161)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump @eslint/js from 9.35.0 to 9.36.0 (#160)

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.35.0 to 9.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.36.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump @stylistic/eslint-plugin from 5.3.1 to 5.4.0 (#159)

Bumps [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic/tree/HEAD/packages/eslint-plugin) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/eslint-stylistic/eslint-stylistic/releases)
- [Changelog](https://github.com/eslint-stylistic/eslint-stylistic/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint-stylistic/eslint-stylistic/commits/v5.4.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@stylistic/eslint-plugin"
  dependency-version: 5.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump react-dom and @types/react-dom (#169)

Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

* Bump @fluentui/react-components from 9.70.0 to 9.72.0 (#171)

Bumps [@fluentui/react-components](https://github.com/microsoft/fluentui) from 9.70.0 to 9.72.0.
- [Release notes](https://github.com/microsoft/fluentui/releases)
- [Changelog](https://github.com/microsoft/fluentui/blob/master/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/fluentui/compare/@fluentui/react-components_v9.70.0...@fluentui/react-components_v9.72.0)

---
updated-dependencies:
- dependency-name: "@fluentui/react-components"
  dependency-version: 9.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Bump eslint from 9.36.0 to 9.37.0 (#170)

Bumps [eslint](https://github.com/eslint/eslint) from 9.36.0 to 9.37.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.36.0...v9.37.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* Revert "chore(deps): Bump @wxt-dev/analytics from 0.4.1 to 0.5.1 (#164)"

This reverts commit 88178035cb.

* chore: update return type for a component

* Bump react and @types/react (#167)

Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/react"
  dependency-version: 19.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

* chore: 3.1.1 version update

* fix(dev): regenerate lockfile

* fix: remove ts-expect-error

* fix: tabs closing before saved #178

* fix: cloud conflict error appears each time when saving a collection after removing all saved ones #180

* feat: option to disable partial save notifications #181

* fix: cloud collection storage retreival fails #180

* chore: 3.2.0 manifest version

* Bump github/codeql-action from 3 to 4 (#183)

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* chore(deps): Bump the deps group with 11 updates (#195)

Bumps the deps group with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [@fluentui/react-components](https://github.com/microsoft/fluentui) | `9.72.0` | `9.72.6` |
| [@fluentui/react-icons](https://github.com/microsoft/fluentui-system-icons) | `2.0.311` | `2.0.313` |
| [@wxt-dev/analytics](https://github.com/wxt-dev/wxt/tree/HEAD/packages/analytics) | `0.4.1` | `0.5.1` |
| [@eslint/css](https://github.com/eslint/css) | `0.11.1` | `0.14.1` |
| [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) | `9.37.0` | `9.39.1` |
| [@eslint/json](https://github.com/eslint/json) | `0.13.2` | `0.14.0` |
| [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic/tree/HEAD/packages/eslint-plugin) | `5.4.0` | `5.5.0` |
| [eslint](https://github.com/eslint/eslint) | `9.37.0` | `9.39.1` |
| [globals](https://github.com/sindresorhus/globals) | `16.4.0` | `16.5.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.45.0` | `8.46.4` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.1.9` | `7.2.2` |


Updates `@fluentui/react-components` from 9.72.0 to 9.72.6
- [Release notes](https://github.com/microsoft/fluentui/releases)
- [Changelog](https://github.com/microsoft/fluentui/blob/master/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/fluentui/compare/@fluentui/react-components_v9.72.0...@fluentui/react-components_v9.72.6)

Updates `@fluentui/react-icons` from 2.0.311 to 2.0.313
- [Changelog](https://github.com/microsoft/fluentui-system-icons/blob/main/fluentui-android-system-icons-release.yml)
- [Commits](https://github.com/microsoft/fluentui-system-icons/commits)

Updates `@wxt-dev/analytics` from 0.4.1 to 0.5.1
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/analytics/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/v0.5.1/packages/analytics)

Updates `@eslint/css` from 0.11.1 to 0.14.1
- [Release notes](https://github.com/eslint/css/releases)
- [Changelog](https://github.com/eslint/css/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/css/compare/css-v0.11.1...css-v0.14.1)

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

Updates `@eslint/json` from 0.13.2 to 0.14.0
- [Release notes](https://github.com/eslint/json/releases)
- [Changelog](https://github.com/eslint/json/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/json/compare/json-v0.13.2...json-v0.14.0)

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

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

Updates `globals` from 16.4.0 to 16.5.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.4.0...v16.5.0)

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

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

---
updated-dependencies:
- dependency-name: "@fluentui/react-components"
  dependency-version: 9.72.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: "@fluentui/react-icons"
  dependency-version: 2.0.313
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: deps
- dependency-name: "@wxt-dev/analytics"
  dependency-version: 0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: "@eslint/css"
  dependency-version: 0.14.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: "@eslint/js"
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: "@eslint/json"
  dependency-version: 0.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: "@stylistic/eslint-plugin"
  dependency-version: 5.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: eslint
  dependency-version: 9.39.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: globals
  dependency-version: 16.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: typescript-eslint
  dependency-version: 8.46.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
- dependency-name: vite
  dependency-version: 7.2.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: deps
...

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

* chore(deps): Bump the react group with 2 updates (#196)

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


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

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

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

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Dustin Jiang <dustinjiang@outlook.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-11 16:05:50 +03:00
xfox111 816c8bf28c chore: update dependabot config to combine npm bumps 2025-11-11 15:37:55 +03:00
xfox111 c27c9f7b33 chore: Patch 3.1.1 (#172)
* chore(loc): zh_CN translation improvements (#153)

* chore(deps): Bump typescript-eslint from 8.43.0 to 8.45.0 (#166)

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.43.0 to 8.45.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.45.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.45.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump @wxt-dev/analytics from 0.4.1 to 0.5.1 (#164)

Bumps [@wxt-dev/analytics](https://github.com/wxt-dev/wxt/tree/HEAD/packages/analytics) from 0.4.1 to 0.5.1.
- [Release notes](https://github.com/wxt-dev/wxt/releases)
- [Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/analytics/CHANGELOG.md)
- [Commits](https://github.com/wxt-dev/wxt/commits/v0.5.1/packages/analytics)

---
updated-dependencies:
- dependency-name: "@wxt-dev/analytics"
  dependency-version: 0.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump eslint from 9.35.0 to 9.36.0 (#163)

Bumps [eslint](https://github.com/eslint/eslint) from 9.35.0 to 9.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.35.0...v9.36.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump globals from 16.3.0 to 16.4.0 (#158)

Bumps [globals](https://github.com/sindresorhus/globals) from 16.3.0 to 16.4.0.
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v16.3.0...v16.4.0)

---
updated-dependencies:
- dependency-name: globals
  dependency-version: 16.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump @eslint/css from 0.11.0 to 0.11.1 (#157)

Bumps [@eslint/css](https://github.com/eslint/css) from 0.11.0 to 0.11.1.
- [Release notes](https://github.com/eslint/css/releases)
- [Changelog](https://github.com/eslint/css/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/css/compare/css-v0.11.0...css-v0.11.1)

---
updated-dependencies:
- dependency-name: "@eslint/css"
  dependency-version: 0.11.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump @fluentui/react-icons from 2.0.309 to 2.0.311 (#156)

Bumps [@fluentui/react-icons](https://github.com/microsoft/fluentui-system-icons) from 2.0.309 to 2.0.311.
- [Changelog](https://github.com/microsoft/fluentui-system-icons/blob/main/fluentui-android-system-icons-release.yml)
- [Commits](https://github.com/microsoft/fluentui-system-icons/commits)

---
updated-dependencies:
- dependency-name: "@fluentui/react-icons"
  dependency-version: 2.0.311
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump typescript from 5.9.2 to 5.9.3 (#165)

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.2 to 5.9.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 5.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump vite from 7.1.5 to 7.1.7 (#161)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

* chore(deps): Bump @eslint/js from 9.35.0 to 9.36.0 (#160)

Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.35.0 to 9.36.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.36.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-version: 9.36.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump @stylistic/eslint-plugin from 5.3.1 to 5.4.0 (#159)

Bumps [@stylistic/eslint-plugin](https://github.com/eslint-stylistic/eslint-stylistic/tree/HEAD/packages/eslint-plugin) from 5.3.1 to 5.4.0.
- [Release notes](https://github.com/eslint-stylistic/eslint-stylistic/releases)
- [Changelog](https://github.com/eslint-stylistic/eslint-stylistic/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint-stylistic/eslint-stylistic/commits/v5.4.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@stylistic/eslint-plugin"
  dependency-version: 5.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* chore(deps): Bump react-dom and @types/react-dom (#169)

Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

* Bump @fluentui/react-components from 9.70.0 to 9.72.0 (#171)

Bumps [@fluentui/react-components](https://github.com/microsoft/fluentui) from 9.70.0 to 9.72.0.
- [Release notes](https://github.com/microsoft/fluentui/releases)
- [Changelog](https://github.com/microsoft/fluentui/blob/master/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/fluentui/compare/@fluentui/react-components_v9.70.0...@fluentui/react-components_v9.72.0)

---
updated-dependencies:
- dependency-name: "@fluentui/react-components"
  dependency-version: 9.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Bump eslint from 9.36.0 to 9.37.0 (#170)

Bumps [eslint](https://github.com/eslint/eslint) from 9.36.0 to 9.37.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.36.0...v9.37.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.37.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

* Revert "chore(deps): Bump @wxt-dev/analytics from 0.4.1 to 0.5.1 (#164)"

This reverts commit 88178035cb.

* chore: update return type for a component

* Bump react and @types/react (#167)

Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

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

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

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: "@types/react"
  dependency-version: 19.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

* chore: 3.1.1 version update

* fix(dev): regenerate lockfile

* fix: remove ts-expect-error

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Dustin Jiang <dustinjiang@outlook.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-05 18:47:38 +05:00
Gitoffthelawn def52278ff docs: i18n for AMO link (#152) 2025-09-09 17:07:23 +03:00
xfox111 101a72e6e3 fix(ci): switch web-ext linting action 2025-09-09 12:57:37 +03:00
xfox111 e21022d985 feat: Minor 3.1.0 (#150)
* Some features are now optional (#148)

* fix(dev): yarn.lock tree fix

* feat: bookmarks moved to optional permissions

* fix: analytics not working in firefox

* feat!: ability to turn off analytics (uses permissions on firefox)

* feat: analytics tracker for bookmark export

* feat: add privacy policy link in about section

* docs: privacy policy update

* feat: ability to chain multiple dialogs

* fix(loc): analytics option translation

* feat: settings review dialog

* fix: background script fails to load because of frontend code

* chore: use analytics permission as storage value

* fix: inverted analytics value

* feat!: option to disable thumbnail capture

* fix(ci): sed typo

* fix: minor fixes

* fix(firefox): web-ext lint error fix

* chore(ci): switch web-ext action

* chore(lint): fix eslint warnings

* chore(deps): monthly dependency bump (September 2025) (#149)

* chore: 3.1.0 version bump

* chore: minor cleanup

* fix: allow analytics checkbox stays inactive after denying permission on firefox

* fix(deps): yarn.lock rebuild

* fix: type assertion for userId

* fix: settings review dialog not showing if welcome dialog is not required

* fix: analytics and thumbnail capture toggles react incorrectly if permission is denied
2025-09-09 12:24:01 +03:00
xfox111 735089eb59 docs: convert store description from html to markdown for Firefox 2025-07-30 16:36:10 +03:00
xfox111 29a9d57348 docs: ci badge fix in readme 2025-07-30 16:35:39 +03:00
xfox111 2bd9337e63 Major 3.0 (#118)
Co-authored-by: Maison da Silva <maisonmdsgreen@hotmail.com>
2025-07-30 15:02:26 +03:00
106 changed files with 12290 additions and 7577 deletions
+2
View File
@@ -0,0 +1,2 @@
* @XFox111
locales/pt_BR.yml @maisondasilva @XFox111
+30 -10
View File
@@ -12,21 +12,43 @@ updates:
directory: "/" # Location of package manifests
target-branch: "next"
assignees:
- "xfox111"
reviewers:
- "xfox111"
- "XFox111"
schedule:
interval: monthly
rebase-strategy: disabled
open-pull-requests-limit: 20
groups:
deps:
patterns:
- "*"
exclude-patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
- "scheduler"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
update-types:
- minor
- patch
react-next:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
update-types:
- major
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "next"
assignees:
- "xfox111"
reviewers:
- "xfox111"
- "XFox111"
schedule:
interval: monthly
rebase-strategy: disabled
@@ -36,9 +58,7 @@ updates:
directory: "/"
target-branch: "next"
assignees:
- "xfox111"
reviewers:
- "xfox111"
- "XFox111"
schedule:
interval: monthly
rebase-strategy: disabled
+22 -5
View File
@@ -38,7 +38,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: node:20
container: node:24
strategy:
fail-fast: false
matrix:
@@ -47,7 +47,23 @@ jobs:
steps:
- uses: actions/checkout@main
- run: |
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: corepack enable
- run: yarn install
# Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043)
- run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js
working-directory: ./node_modules/@dnd-kit/core/dist
if: ${{ matrix.target == 'firefox' }}
# Patch for firefox analytics (see https://github.com/wxt-dev/wxt/pull/1808)
- run: sed -i 's|if (location.pathname === "/background.js")|if (location.pathname === "/background.js" \|\| location.pathname === "/_generated_background_page.html")|' index.mjs
working-directory: ./node_modules/@wxt-dev/analytics/dist
if: ${{ matrix.target == 'firefox' }}
- run: yarn zip -b ${{ matrix.target }}
- name: Drop build artifacts (${{ matrix.target }})
@@ -59,12 +75,13 @@ jobs:
- name: web-ext lint
if: ${{ matrix.target == 'firefox' }}
uses: freaktechnik/web-ext-lint@main
uses: kewisch/action-web-ext@main
with:
extension-root: ./.output/firefox-mv3
self-hosted: false
cmd: lint
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' }}
publish-github:
+4 -4
View File
@@ -52,11 +52,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@main
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,4 +83,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
extver=`jq -r ".version" package.json`
echo "version=$extver" >> "$GITHUB_OUTPUT"
- uses: dev-build-deploy/release-me@v0.18.0
- uses: dev-build-deploy/release-me@v0.18.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
prefix: v
+25 -6
View File
@@ -5,9 +5,11 @@ on:
branches: [ "main", "next" ]
paths-ignore:
- '**.md'
- '**.txt'
- "locales/*"
- 'LICENSE'
- 'PRIVACY'
- '**/cd_pipeline.yaml'
- '**/cd_pipeline.yml'
- '**/dependabot.yml'
- '**/codeql-analysis.yml'
- '**/pr_next.yaml'
@@ -28,7 +30,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: node:23
container: node:24
strategy:
fail-fast: false
matrix:
@@ -37,7 +39,23 @@ jobs:
steps:
- uses: actions/checkout@main
- run: |
echo "WXT_GA4_API_SECRET=${{ secrets.GA4_SECRET }}" >> .env
echo "WXT_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}" >> .env
- run: corepack enable
- run: yarn install
# Patch for firefox dnd popup (see https://github.com/clauderic/dnd-kit/issues/1043)
- run: grep -v "this.windowListeners.add(EventName.Resize, this.handleCancel);" core.esm.js > core.esm.js.tmp && mv core.esm.js.tmp core.esm.js
working-directory: ./node_modules/@dnd-kit/core/dist
if: ${{ matrix.target == 'firefox' }}
# Patch for firefox analytics (see https://github.com/wxt-dev/wxt/pull/1808)
- run: sed -i 's|if (location.pathname === "/background.js")|if (location.pathname === "/background.js" \|\| location.pathname === "/_generated_background_page.html")|' index.mjs
working-directory: ./node_modules/@wxt-dev/analytics/dist
if: ${{ matrix.target == 'firefox' }}
- run: yarn zip -b ${{ matrix.target }}
- name: Drop artifacts (${{ matrix.target }})
@@ -49,9 +67,10 @@ jobs:
- name: web-ext lint
if: ${{ matrix.target == 'firefox' }}
uses: freaktechnik/web-ext-lint@main
uses: kewisch/action-web-ext@main
with:
extension-root: ./.output/firefox-mv3
self-hosted: false
cmd: lint
source: ./.output/firefox-mv3
channel: listed
- run: yarn audit
- run: yarn npm audit
+3
View File
@@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
.yarn/
.output
stats.html
stats-*.json
@@ -26,3 +27,5 @@ web-ext.config.ts
*.sw?
web-ext.config.js
.env*
+117
View File
@@ -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"
+49 -4
View File
@@ -1,8 +1,53 @@
# Tabs aside extension Privacy policy
1. Developers of the extension don't affiliate with Google LLC, Mozilla Foundation or Microsoft Corporation in any way.
2. This extension only stores user data related to its core functionality. This includes:
2. This extension stores user data only related to its core functionality. This includes:
- User settings
- User saved collections of tabs
2. This extension doesn't use any tracking software, nor does it collect, sell or share any personal data with any third parties.
3. This extension uses cloud storage built into your browser to store its data.
4. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
- Thumbnails of saved tabs
3. This extension uses Google Analytics to collect usage statistics and improve the extension.
4. This extension uses analytics to collect following data:
- Random UUID to distinguish unique users
- Browser name and version
- Operating system name and version
- System architecture
- Screen resolution
- Extension language
- User settings
- Number of saved collections
- Events, related to user's actions:
- `bmc_clicked` (when "Buy me a Coffee" button is clicked)
- `collection_list` (when extension's options page is opened)
- `cta_dismissed` (when "Like this extension?" prompt is closed)
- `extension_installed` (when extension is installed or updated)
- `feedback_clicked` (when "Leave feedback" button is clicked)
- `item_created` (when new collection or group is created using dialog window)
- `item_edited` (when collection or group is edited)
- `options_page` (when extension's options page is opened)
- `page_view` (when extension's page is opened)
- `save` (when "Save all tabs" or "Save selected tabs" buttons are clicked)
- `set_aside` (when "Set all tabs aside" or "Set selected tabs aside" buttons are clicked)
- `used_drag_and_drop` (when items inside collection list were reordered)
- `visit_blog_button_click` (when "Read dev blog" button is clicked)
- `bookmarks_saved` (when "Export to bookmarks" option is clicked)
- Events, related to extension errors:
- `background_error` (when error inside background service has occured)
- `cloud_get_error` (when failed to retrieve collections from the cloud storage)
- `conflict_resolve_with_cloud_error` (when failed to retrieve collections from the cloud storage during storage conflict resolution)
- `cloud_save_error` (when failed to save collections to the cloud storage)
- `messaging_error` (when failed to send a message to extenion's background service)
- `notification_error` (when failed to display a toast notification)
4. Following events, beside their name, include additional information, such as:
- `item_created` and `item_edited`:
- Type of the affected item (`collection` or `group`)
- `extension_installed`:
- Reason for update (`install`, `update`, or `browser_update`)
- Previously installed extension's version, if applicable
- `page_view`:
- Type of the page (`options_page` or `collection_list`)
- All extension's error events:
- Error name
- Error message
- Error call stack
4. This extension does not collect or use any personally identifiable information (PII) or sensitive data for purposes other than its core functionality.
5. This extension uses cloud storage built into your browser to store its data.
6. Refer to your browser's developer regarding the privacy policy of the cloud storage used by your browser.
+3 -3
View File
@@ -26,7 +26,7 @@ Check out our [latest blog post](https://at.xfox111.net/tabs-aside-3-0) regardin
- English
- Italian
- Polish
- Portuguese (Brazil)
- Portuguese (Brazil) by [@maisondasilva](https://github.com/maisondasilva)
- Russian
- Spanish
- Ukrainian
@@ -37,7 +37,7 @@ Check out our [latest blog post](https://at.xfox111.net/tabs-aside-3-0) regardin
- [Google Chrome Webstore](https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdpbdjgin)
- [Microsoft Edge Add-ons Webstore](https://microsoftedge.microsoft.com/addons/detail/kmnblllmalkiapkfknnlpobmjjdnlhnd)
- [Firefox Add-ons](https://addons.mozilla.org/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)
### 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
[![GitHub issues](https://img.shields.io/github/issues/xfox111/TabsAsideExtension)](https://github.com/xfox111/TabsAsideExtension/issues)
[![CI](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yaml/badge.svg)](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yaml)
[![CI](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yml/badge.svg)](https://github.com/XFox111/TabsAsideExtension/actions/workflows/cd_pipeline.yml)
[![GitHub repo size](https://img.shields.io/github/repo-size/xfox111/TabsAsideExtension?label=repo%20size)](https://github.com/xfox111/TabsAsideExtension)
There are many ways in which you can participate in the project, for example:
+3
View File
@@ -39,16 +39,19 @@ body
/* Handle */
::-webkit-scrollbar-thumb
{
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover
{
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1Hover);
}
::-webkit-scrollbar-thumb:hover:active
{
/* eslint-disable-next-line css/no-invalid-properties */
background-color: var(--colorNeutralStroke1Pressed);
}
+4 -3
View File
@@ -13,12 +13,13 @@ export const githubLinks =
repo: githubLink(),
release: githubLink(`releases/tag/v${Package.version}`),
license: githubLink("blob/main/LICENSE"),
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations")
translationGuide: githubLink("wiki/Contribution-Guidelines#contributing-to-translations"),
privacy: githubLink("blob/main/PRIVACY.md")
};
export const storeLink: string =
import.meta.env.FIREFOX
? "https://addons.mozilla.org/en-US/firefox/addon/ms-edge-tabs-aside/" :
chrome.runtime.getManifest().update_url?.startsWith("https://edge.microsoft.com/") ?
browser.runtime.getManifest().update_url?.startsWith("https://edge.microsoft.com/") ?
"https://microsoftedge.microsoft.com/addons/detail/tabs-aside/kmnblllmalkiapkfknnlpobmjjdnlhnd" :
"https://chrome.google.com/webstore/detail/mgmjbodjgijnebfgohlnjkegdbdjgin";
"https://chromewebstore.google.com/detail/tabs-aside/mgmjbodjgijnebfgohlnjkegdpbdjgin";
+203 -62
View File
@@ -1,23 +1,32 @@
import { collectionCount, getCollections, saveCollections } from "@/features/collectionStorage";
import { track, trackError } from "@/features/analytics";
import { collectionCount, getCollections, saveCollections, thumbnailCaptureEnabled } from "@/features/collectionStorage";
import { collectionStorage } from "@/features/collectionStorage/utils/collectionStorage";
import getCollectionsFromCloud from "@/features/collectionStorage/utils/getCollectionsFromCloud";
import getCollectionsFromLocal from "@/features/collectionStorage/utils/getCollectionsFromLocal";
import { migrateStorage } from "@/features/migration";
import { setSettingsReviewNeeded } from "@/features/settingsReview/utils";
import { showWelcomeDialog } from "@/features/v3welcome/utils/showWelcomeDialog";
import { SettingsValue } from "@/hooks/useSettings";
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
import { closeTabsAsync } from "@/utils/closeTabsAsync";
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
import getLogger from "@/utils/getLogger";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
import { onMessage, sendMessage } from "@/utils/messaging";
import saveTabsToCollection from "@/utils/saveTabsToCollection";
import sendNotification from "@/utils/sendNotification";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
import { settings } from "@/utils/settings";
import watchTabSelection from "@/utils/watchTabSelection";
import { Tabs, Windows } from "wxt/browser";
import { Unwatch } from "wxt/storage";
import { RemoveListenerCallback } from "@webext-core/messaging";
import { Unwatch } from "wxt/utils/storage";
import { openCollection, openGroup } from "./sidepanel/utils/opener";
export default defineBackground(() =>
{
try
{
const logger = getLogger("background");
const graphicsCache: GraphicsStorage = {};
let graphicsCache: GraphicsStorage = {};
let listLocation: SettingsValue<"listLocation"> = "sidebar";
logger("Background script started");
@@ -30,27 +39,44 @@ export default defineBackground(() =>
browser.runtime.onInstalled.addListener(async ({ reason, previousVersion }) =>
{
logger("onInstalled", reason, previousVersion);
track("extension_installed", { reason, previousVersion: previousVersion ?? "none" });
const previousMajor: number = previousVersion ? parseInt(previousVersion.split(".")[0]) : 0;
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
const cumulative: number = major * 10000 + minor * 100 + patch;
if (reason === "update" && previousMajor < 3)
await setSettingsReviewNeeded(reason, previousVersion);
if (reason === "update" && cumulative < 30000) // < 3.0.0
{
await migrateStorage();
await showWelcomeDialog.setValue(true);
browser.runtime.reload();
}
});
browser.tabs.onUpdated.addListener((_, __, tab) =>
{
if (!tab.url)
return;
if (reason === "update" && cumulative >= 30000 && cumulative < 30200) // >= 3.0.0 && < 3.2.0
{
// Merge cloud and local storage if they are out of sync
const localTimestamp: number = await collectionStorage.localLastUpdated.getValue();
const syncTimestamp: number = await collectionStorage.syncLastUpdated.getValue();
graphicsCache[tab.url] = {
preview: graphicsCache[tab.url]?.preview,
capture: graphicsCache[tab.url]?.capture,
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
};
if (localTimestamp === syncTimestamp)
return;
try
{
const localCollections: CollectionItem[] = await getCollectionsFromLocal();
const cloudCollections: CollectionItem[] = await getCollectionsFromCloud();
const mergedCollections: CollectionItem[] = [...cloudCollections, ...localCollections];
await saveCollections(mergedCollections, true, graphicsCache);
}
catch (ex)
{
logger("Failed to merge cloud and local storage during update");
trackError("cloud_sync_merge_error", ex as Error);
console.error(ex);
}
}
});
browser.commands.onCommand.addListener(
@@ -58,28 +84,45 @@ export default defineBackground(() =>
);
onMessage("getGraphicsCache", () => graphicsCache);
onMessage("addThumbnail", ({ data }) =>
onMessage("refreshCollections", () => { });
if (import.meta.env.FIREFOX)
{
graphicsCache[data.url] = {
preview: data.thumbnail,
capture: graphicsCache[data.url]?.capture,
icon: graphicsCache[data.url]?.icon
};
});
onMessage("openCollection", ({ data }) => openCollection(data.collection, data.targetWindow));
onMessage("openGroup", ({ data }) => openGroup(data.group, data.newWindow));
}
setupTabCaputre();
async function setupTabCaputre(): Promise<void>
{
const tryCaptureTab = async (tab: Tabs.Tab): Promise<void> =>
let unwatchAddThumbnail: RemoveListenerCallback | null = null;
let captureInterval: NodeJS.Timeout | null = null;
const captureFavicon = (_: any, __: any, tab: Browser.tabs.Tab): void =>
{
if (!tab.url)
return;
graphicsCache[tab.url] = {
preview: graphicsCache[tab.url]?.preview,
capture: graphicsCache[tab.url]?.capture,
icon: tab.favIconUrl ?? graphicsCache[tab.url]?.icon
};
};
const tryCaptureTab = async (tab: Browser.tabs.Tab): Promise<void> =>
{
if (!tab.url || tab.status !== "complete" || !tab.active)
return;
if (graphicsCache[tab.url]?.capture || graphicsCache[tab.url]?.capture === null)
return;
try
{
// We use chrome here because polyfill throws uncatchable errors for some reason
// It's a compatible API anyway
const capture: string = await chrome.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
const capture: string = await browser.tabs.captureVisibleTab(tab.windowId!, { format: "jpeg", quality: 1 });
if (capture)
{
@@ -88,18 +131,73 @@ export default defineBackground(() =>
preview: graphicsCache[tab.url]?.preview,
icon: graphicsCache[tab.url]?.icon
};
logger("Captured tab", tab.url);
}
}
catch (ex) { logger(ex); }
catch
{
graphicsCache[tab.url] = {
capture: null!,
preview: graphicsCache[tab.url]?.preview,
icon: graphicsCache[tab.url]?.icon
};
}
};
setInterval(() =>
const updateCapture = async (captureThumbnails: boolean): Promise<void> =>
{
browser.tabs.query({ active: true })
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
}, 1000);
const scriptingGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"] });
if (captureThumbnails)
{
if (scriptingGranted)
await browser.scripting.registerContentScripts([
{
id: "capture-script",
matches: ["<all_urls>"],
runAt: "document_idle",
js: ["capture.js"]
}
]);
unwatchAddThumbnail = onMessage("addThumbnail", ({ data }) =>
{
graphicsCache[data.url] = {
preview: data.thumbnail,
capture: graphicsCache[data.url]?.capture,
icon: graphicsCache[data.url]?.icon
};
});
captureInterval = setInterval(() =>
{
browser.tabs.query({ active: true })
.then(tabs => tabs.forEach(tab => tryCaptureTab(tab)));
}, 1000);
browser.tabs.onUpdated.addListener(captureFavicon);
}
else
{
if (scriptingGranted)
await browser.scripting.unregisterContentScripts({
ids: ["capture-script"]
});
unwatchAddThumbnail?.();
if (captureInterval)
clearInterval(captureInterval);
browser.tabs.onUpdated.removeListener(captureFavicon);
graphicsCache = {};
}
};
if (await thumbnailCaptureEnabled.getValue())
updateCapture(true);
thumbnailCaptureEnabled.watch(updateCapture);
}
setupContextMenu();
@@ -188,6 +286,7 @@ export default defineBackground(() =>
};
const toggleSidebarFirefox = async (): Promise<void> =>
// @ts-expect-error Firefox-only API
await browser.sidebarAction.toggle();
const updateButton = async (action: SettingsValue<"contextAction">): Promise<void> =>
@@ -204,7 +303,7 @@ export default defineBackground(() =>
unwatchActionTitle?.();
if (!import.meta.env.FIREFOX)
await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
await browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: false });
// Setup new behavior
if (action === "action")
@@ -223,7 +322,7 @@ export default defineBackground(() =>
if (import.meta.env.FIREFOX)
browser.action.onClicked.addListener(toggleSidebarFirefox);
else
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
browser.sidePanel.setPanelBehavior({ openPanelOnActionClick: true });
}
else if (location !== "popup")
browser.action.onClicked.addListener(openCollectionsInTab);
@@ -238,24 +337,35 @@ export default defineBackground(() =>
setupCollectionView();
async function setupCollectionView(): Promise<void>
{
const enforcePinnedTab = async (info: Tabs.OnHighlightedHighlightInfoType): Promise<void> =>
const enforcePinnedTab = async (): Promise<void> =>
{
logger("enforcePinnedTab", info);
logger("enforcePinnedTab");
const activeWindow: Windows.Window = await browser.windows.getCurrent({ populate: true });
const openWindows: Browser.windows.Window[] = await browser.windows.getAll({ populate: true });
if (activeWindow.incognito)
return;
for (const openWindow of openWindows)
{
if (openWindow.incognito || openWindow.type !== "normal")
continue;
if (!activeWindow.tabs!.some(tab =>
[tab.url, tab.pendingUrl].includes(browser.runtime.getURL("/sidepanel.html")))
)
await browser.tabs.create({
url: browser.runtime.getURL("/sidepanel.html"),
windowId: activeWindow.id,
active: false,
pinned: true
});
const activeTabs: Browser.tabs.Tab[] = openWindow.tabs!.filter(tab =>
tab.url === browser.runtime.getURL("/sidepanel.html"));
const targetTab: Browser.tabs.Tab | undefined = activeTabs.find(tab => tab.pinned);
if (!targetTab)
await browser.tabs.create({
url: browser.runtime.getURL("/sidepanel.html"),
windowId: openWindow.id,
active: false,
pinned: true
});
const tabsToClose: Browser.tabs.Tab[] = activeTabs.filter(tab => tab.id !== targetTab?.id);
if (tabsToClose.length > 0)
await browser.tabs.remove(tabsToClose.map(tab => tab.id!));
}
};
const updateView = async (viewLocation: SettingsValue<"listLocation">): Promise<void> =>
@@ -263,8 +373,7 @@ export default defineBackground(() =>
logger("updateView", viewLocation);
browser.tabs.onHighlighted.removeListener(enforcePinnedTab);
const tabs: Tabs.Tab[] = await browser.tabs.query({
currentWindow: true,
const tabs: Browser.tabs.Tab[] = await browser.tabs.query({
url: browser.runtime.getURL("/sidepanel.html")
});
await browser.tabs.remove(tabs.map(tab => tab.id!));
@@ -274,19 +383,16 @@ export default defineBackground(() =>
});
if (import.meta.env.FIREFOX)
// @ts-expect-error Firefox-only API
await browser.sidebarAction.setPanel({
panel: viewLocation === "sidebar" ? browser.runtime.getURL("/sidepanel.html") : ""
});
else
await chrome.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
await browser.sidePanel.setOptions({ enabled: viewLocation === "sidebar" });
if (viewLocation === "pinned")
{
await browser.tabs.create({
url: browser.runtime.getURL("/sidepanel.html"),
active: false,
pinned: true
});
enforcePinnedTab();
browser.tabs.onHighlighted.addListener(enforcePinnedTab);
}
};
@@ -313,9 +419,10 @@ export default defineBackground(() =>
if (view === "sidebar")
{
if (import.meta.env.FIREFOX)
// @ts-expect-error Firefox-only API
browser.sidebarAction.open();
else
chrome.sidePanel.open({ windowId });
browser.sidePanel.open({ windowId });
}
else
browser.action.openPopup();
@@ -325,10 +432,29 @@ export default defineBackground(() =>
{
logger("openCollectionsInTab");
const currentWindow: Windows.Window = await browser.windows.getCurrent({ populate: true });
const currentWindow: Browser.windows.Window = await browser.windows.getCurrent({ populate: true });
if (currentWindow.incognito)
{
let availableWindows: Browser.windows.Window[] = await browser.windows.getAll({ populate: true });
availableWindows = availableWindows.filter(window =>
!window.incognito &&
window.tabs?.some(i => i.url === browser.runtime.getURL("/sidepanel.html"))
);
if (availableWindows.length > 0)
{
const availableTab: Browser.tabs.Tab = availableWindows[0].tabs!.find(
tab => tab.url === browser.runtime.getURL("/sidepanel.html")
)!;
await browser.tabs.update(availableTab.id, { active: true });
await browser.windows.update(availableWindows[0].id!, { focused: true });
return;
}
await browser.windows.create({
url: browser.runtime.getURL("/sidepanel.html"),
focused: true
@@ -336,7 +462,7 @@ export default defineBackground(() =>
}
else
{
const collectionTab: Tabs.Tab | undefined = currentWindow.tabs!.find(
const collectionTab: Browser.tabs.Tab | undefined = currentWindow.tabs!.find(
tab => tab.url === browser.runtime.getURL("/sidepanel.html")
);
@@ -355,14 +481,28 @@ export default defineBackground(() =>
{
logger("saveTabs", closeAfterSave);
const collection: CollectionItem = await saveTabsToCollection(closeAfterSave);
const [tabs, skipCount] = await getTabsToSaveAsync();
if (tabs.length < 1)
{
await sendPartialSaveNotification();
return;
}
const collection: CollectionItem = await createCollectionFromTabs(tabs);
const [savedCollections, cloudIssue] = await getCollections();
const newList = [collection, ...savedCollections];
await saveCollections(newList, cloudIssue === null, graphicsCache);
track(closeAfterSave ? "set_aside" : "save");
sendMessage("refreshCollections", undefined);
if (skipCount > 0)
await sendPartialSaveNotification();
if (closeAfterSave)
await closeTabsAsync(tabs);
if (await settings.notifyOnSave.getValue())
await sendNotification({
title: i18n.t("notifications.tabs_saved.title"),
@@ -374,5 +514,6 @@ export default defineBackground(() =>
catch (ex)
{
console.error(ex);
trackError("background_error", ex as Error);
}
});
@@ -4,11 +4,7 @@ import { sendMessage } from "@/utils/messaging";
// This content script is injected into each browser tab.
// It's purpose is to retrive an OpenGraph thumbnail URL from the metadata
export default defineContentScript({
matches: ["<all_urls>"],
runAt: "document_idle",
main
});
export default defineUnlistedScript({ main });
const logger = getLogger("contentScript");
@@ -34,5 +34,12 @@ export const useOptionsStyles = makeStyles({
display: "flex",
flexWrap: "wrap",
gap: tokens.spacingHorizontalS
},
group:
{
display: "flex",
flexFlow: "column",
alignItems: "flex-start",
gap: tokens.spacingVerticalSNudge
}
});
+2 -1
View File
@@ -34,7 +34,8 @@ export default function AboutSection(): React.ReactElement
<Body1 as="p">
<Link { ...extLink(websiteLink) }>{ i18n.t("options_page.about.links.website") }</Link><br />
<Link { ...extLink(githubLinks.repo) }>{ i18n.t("options_page.about.links.source") }</Link><br />
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link>
<Link { ...extLink(githubLinks.release) }>{ i18n.t("options_page.about.links.changelog") }</Link><br />
<Link { ...extLink(githubLinks.privacy) }>{ i18n.t("options_page.about.links.privacy") }</Link>
</Body1>
<div className={ cls.horizontalButtons }>
@@ -1,3 +1,4 @@
import { analyticsPermission } from "@/features/analytics";
import useSettings, { SettingsValue } from "@/hooks/useSettings";
import { Button, Checkbox, Dropdown, Field, Option, OptionOnSelectData } from "@fluentui/react-components";
import { KeyCommand20Regular } from "@fluentui/react-icons";
@@ -13,9 +14,25 @@ export default function GeneralSection(): React.ReactElement
const [dismissOnLoad, setDismissOnLoad] = useSettings("dismissOnLoad");
const [listLocation, setListLocation] = useSettings("listLocation");
const [contextAction, setContextAction] = useSettings("contextAction");
const [showPartialSaveNotification, setShowPartialSaveNotification] = useSettings("showPartialSaveNotification");
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
const cls = useOptionsStyles();
useEffect(() =>
{
analyticsPermission.getValue().then(setAllowAnalytics);
return analyticsPermission.watch(setAllowAnalytics);
}, []);
const updateAnalytics = (enabled: boolean): void =>
{
setAllowAnalytics(null);
analyticsPermission.setValue(enabled)
.catch(() => setAllowAnalytics(!enabled));
};
const openShortcutsPage = (): Promise<any> =>
browser.tabs.create({
url: "chrome://extensions/shortcuts",
@@ -27,6 +44,10 @@ export default function GeneralSection(): React.ReactElement
if (e.optionValue === "popup" && contextAction !== "open")
setContextAction("open");
if (import.meta.env.FIREFOX && e.optionValue !== "sidebar")
// @ts-expect-error Firefox-only API
browser.sidebarAction.close();
setListLocation(e.optionValue as ListLocationType);
};
@@ -53,10 +74,20 @@ export default function GeneralSection(): React.ReactElement
label={ i18n.t("options_page.general.options.show_notification") }
checked={ notifyOnSave ?? false }
onChange={ (_, e) => setNotifyOnSave(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.show_partial_save_notification") }
checked={ showPartialSaveNotification ?? false }
onChange={ (_, e) => setShowPartialSaveNotification(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.unload_tabs") }
checked={ dismissOnLoad ?? false }
onChange={ (_, e) => setDismissOnLoad(e.checked as boolean) } />
<Checkbox
label={ i18n.t("options_page.general.options.allow_analytics") }
checked={ allowAnalytics ?? true }
disabled={ allowAnalytics === null }
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
</section>
<Field label={ i18n.t("options_page.general.options.list_locations.title") }>
+53 -3
View File
@@ -1,9 +1,10 @@
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 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 { Unwatch } from "wxt/utils/storage";
import { useOptionsStyles } from "../hooks/useOptionsStyles";
import exportData from "../utils/exportData";
import importData from "../utils/importData";
@@ -13,6 +14,7 @@ export default function StorageSection(): React.ReactElement
const { bytesInUse, storageQuota, usedStorageRatio } = useStorageInfo();
const [importResult, setImportResult] = useState<boolean | null>(null);
const [isCloudDisabled, setCloudDisabled] = useState<boolean>(null!);
const [isThumbnailCaptureEnabled, setThumbnailCaptureEnabled] = useState<boolean | null>(null);
const dialog = useDialog();
const cls = useOptionsStyles();
@@ -20,10 +22,35 @@ export default function StorageSection(): React.ReactElement
useEffect(() =>
{
thumbnailCaptureEnabled.getValue().then(setThumbnailCaptureEnabled);
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 =>
dialog.pushPrompt({
title: i18n.t("options_page.storage.import_prompt.title"),
@@ -51,6 +78,29 @@ export default function StorageSection(): React.ReactElement
return (
<>
<div className={ cls.group }>
<Switch
checked={ isThumbnailCaptureEnabled ?? true }
disabled={ isThumbnailCaptureEnabled === null }
onChange={ (_, e) => handleSetThumbnailCapture(e.checked as boolean) }
label={ {
children: (_: any, props: LabelProps) =>
<InfoLabel
{ ...props }
label={ i18n.t("options_page.storage.thumbnail_capture") }
info={
<p>
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }<br /><br />
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
</p>
} />
} } />
<Button onClick={ handleClearThumbnails } className={ dangerCls.buttonSubtle } appearance="subtle">
{ i18n.t("options_page.storage.clear_thumbnails.action") }
</Button>
</div>
{ isCloudDisabled === false &&
<Field
label={ i18n.t("options_page.storage.capacity.title") }
+3
View File
@@ -1,5 +1,6 @@
import App from "@/App.tsx";
import "@/assets/global.css";
import { trackPage } from "@/features/analytics";
import { Tab, TabList } from "@fluentui/react-components";
import ReactDOM from "react-dom/client";
import { useOptionsStyles } from "./hooks/useOptionsStyles.ts";
@@ -14,6 +15,8 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
</App>
);
trackPage("options_page");
function OptionsPage(): React.ReactElement
{
const [selection, setSelection] = useState<SelectionType>("general");
+4 -1
View File
@@ -4,13 +4,16 @@ export default async function exportData(): Promise<void>
local: await browser.storage.local.get(null),
sync: await browser.storage.sync.get(null)
});
const blob: Blob = new Blob([data], { type: "application/json" });
const element: HTMLAnchorElement = document.createElement("a");
element.style.display = "none";
element.href = `data:application/json;charset=utf-8,${data}`;
element.href = URL.createObjectURL(blob);
element.setAttribute("download", "tabs-aside_data.json");
document.body.appendChild(element);
element.click();
URL.revokeObjectURL(element.href);
document.body.removeChild(element);
};
+5
View File
@@ -37,7 +37,12 @@ export default async function importData(): Promise<boolean | null>
await browser.storage.local.set(data.local);
if (data.sync)
{
if (import.meta.env.FIREFOX && data.sync.contextAction === "context")
data.sync.contextAction = "open";
await browser.storage.sync.set(data.sync);
}
}
catch (error)
{
@@ -27,7 +27,7 @@ export const useStyles_CollectionView = makeStyles({
},
verticalRoot:
{
height: "560px"
maxHeight: "560px"
},
empty:
{
@@ -74,7 +74,8 @@ export const useStyles_CollectionView = makeStyles({
{
gridAutoFlow: "row",
width: "100%",
paddingBottom: tokens.spacingVerticalS
paddingBottom: tokens.spacingVerticalS,
gridAutoRows: import.meta.env.FIREFOX ? "min-content" : undefined
},
dragOverlay:
{
@@ -33,7 +33,7 @@ export default function CollectionView({ collection, index: collectionIndex, dra
const colorCls = useGroupColors();
return (
<CollectionContext.Provider value={ { collection, collectionIndex, tabCount, hasPinnedGroup } }>
<CollectionContext.Provider value={ { collection, tabCount, hasPinnedGroup } }>
<div
ref={ setNodeRef } { ...nodeProps }
className={ mergeClasses(
@@ -50,26 +50,30 @@ export default function CollectionView({ collection, index: collectionIndex, dra
<CollectionHeader dragHandleProps={ activatorProps } dragHandleRef={ setActivatorNodeRef } />
{ collection.items.length < 1 ?
<div className={ cls.empty }>
<CollectionsRegular fontSize={ 32 } />
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
</div>
:
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
<SortableContext
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
>
{ collection.items.map((i, index) =>
i.type === "group" ?
<GroupView
key={ index } group={ i } indices={ [collectionIndex, index] } />
:
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
) }
</SortableContext>
</div>
{ (!activeItem || activeItem.item.type !== "collection") && !dragOverlay &&
<>
{ collection.items.length < 1 ?
<div className={ cls.empty }>
<CollectionsRegular fontSize={ 32 } />
<Body1Strong>{ i18n.t("collections.empty") }</Body1Strong>
</div>
:
<div className={ mergeClasses(cls.list, !tilesView && cls.verticalList) }>
<SortableContext
items={ collection.items.map((_, index) => [collectionIndex, index].join("/")) }
strategy={ tilesView ? horizontalListSortingStrategy : verticalListSortingStrategy }
>
{ collection.items.map((i, index) =>
i.type === "group" ?
<GroupView
key={ index } group={ i } indices={ [collectionIndex, index] } />
:
<TabView key={ index } tab={ i } indices={ [collectionIndex, index] } />
) }
</SortableContext>
</div>
}
</>
}
</div >
</CollectionContext.Provider>
+88 -73
View File
@@ -1,4 +1,5 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import { track } from "@/features/analytics";
import { useGroupColors } from "@/hooks/useGroupColors";
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
import * as fui from "@fluentui/react-components";
@@ -15,7 +16,7 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
?? ""
);
const [color, setColor] = useState<chrome.tabGroups.ColorEnum | undefined | "pinned">(
const [color, setColor] = useState<`${Browser.tabGroups.Color}` | undefined | "pinned">(
props.type === "collection"
? props.collection?.color :
props.group?.pinned === true ? "pinned" : (props.group?.color ?? "blue")
@@ -23,15 +24,27 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
const cls = useStyles_EditDialog();
const colorCls = useGroupColors();
const horizontalNavigationAttributes = fui.useArrowNavigationGroup({ axis: "horizontal" });
const onSubmit = (e: React.FormEvent<HTMLFormElement>) =>
{
e.preventDefault();
handleSave();
};
const handleSave = () =>
{
if (props.type === "collection" ? props.collection !== null : props.group !== null)
track("item_edited", { type: props.type });
else
track("item_created", { type: props.type });
if (props.type === "collection")
props.onSave({
type: "collection",
timestamp: props.collection?.timestamp ?? Date.now(),
color: (color === "pinned") ? undefined : color!,
title,
title: title ? title : undefined,
items: props.collection?.items ?? []
});
else if (color === "pinned")
@@ -45,85 +58,87 @@ export default function EditDialog(props: GroupEditDialogProps): ReactElement
type: "group",
pinned: false,
color: color!,
title,
title: title ? title : undefined,
items: props.group?.items ?? []
});
};
return (
<fui.DialogSurface className={ fui.mergeClasses(cls.surface, (color && color !== "pinned") && colorCls[color]) }>
<fui.DialogBody>
<fui.DialogTitle>
{
props.type === "collection" ?
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
}
</fui.DialogTitle>
<form onSubmit={ onSubmit }>
<fui.DialogBody>
<fui.DialogTitle>
{
props.type === "collection" ?
i18n.t(`dialogs.edit.title.${props.collection ? "edit" : "new"}_collection`) :
i18n.t(`dialogs.edit.title.${props.group ? "edit" : "new"}_group`)
}
</fui.DialogTitle>
<fui.DialogContent>
<form className={ cls.content }>
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
<fui.Input
contentBefore={ <Rename20Regular /> }
disabled={ color === "pinned" }
placeholder={
props.type === "collection" ? getCollectionTitle(props.collection) : ""
}
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
onChange={ (_, e) => setTitle(e.value) } />
</fui.Field>
<fui.Field label="Color">
<div className={ cls.colorPicker }>
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
<fui.ToggleButton
checked={ color === "pinned" }
onClick={ () => setColor("pinned") }
icon={ <Pin20Filled /> }
shape="circular"
>
{ i18n.t("groups.pinned") }
</fui.ToggleButton>
}
{ props.type === "collection" &&
<fui.ToggleButton
checked={ color === undefined }
onClick={ () => setColor(undefined) }
icon={ <CircleOff20Regular /> }
shape="circular"
>
{ i18n.t("colors.none") }
</fui.ToggleButton>
}
{ Object.keys(colorCls).map(i =>
<fui.ToggleButton
checked={ color === i }
onClick={ () => setColor(i as chrome.tabGroups.ColorEnum) }
className={ fui.mergeClasses(cls.colorButton, colorCls[i as chrome.tabGroups.ColorEnum]) }
icon={ {
className: cls.colorButton_icon,
children: <Circle20Filled />
} }
key={ i }
shape="circular"
>
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
</fui.ToggleButton>
) }
</div>
</fui.Field>
</form>
</fui.DialogContent>
<fui.DialogContent>
<div className={ cls.content }>
<fui.Field label={ i18n.t("dialogs.edit.collection_title") }>
<fui.Input
contentBefore={ <Rename20Regular /> }
disabled={ color === "pinned" }
placeholder={
props.type === "collection" ? getCollectionTitle(props.collection, true) : ""
}
value={ color === "pinned" ? i18n.t("groups.pinned") : title }
onChange={ (_, e) => setTitle(e.value) } />
</fui.Field>
<fui.Field label="Color">
<div className={ cls.colorPicker } { ...horizontalNavigationAttributes }>
{ (props.type === "group" && (!props.hidePinned || props.group?.pinned)) &&
<fui.ToggleButton
checked={ color === "pinned" }
onClick={ () => setColor("pinned") }
icon={ <Pin20Filled /> }
shape="circular"
>
{ i18n.t("groups.pinned") }
</fui.ToggleButton>
}
{ props.type === "collection" &&
<fui.ToggleButton
checked={ color === undefined }
onClick={ () => setColor(undefined) }
icon={ <CircleOff20Regular /> }
shape="circular"
>
{ i18n.t("colors.none") }
</fui.ToggleButton>
}
{ Object.keys(colorCls).map(i =>
<fui.ToggleButton
checked={ color === i }
onClick={ () => setColor(i as `${Browser.tabGroups.Color}`) }
className={ fui.mergeClasses(cls.colorButton, colorCls[i as `${Browser.tabGroups.Color}`]) }
icon={ {
className: cls.colorButton_icon,
children: <Circle20Filled />
} }
key={ i }
shape="circular"
>
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) }
</fui.ToggleButton>
) }
</div>
</fui.Field>
</div>
</fui.DialogContent>
<fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" onClick={ handleSave }>{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
</fui.DialogTrigger>
</fui.DialogActions>
</fui.DialogBody>
<fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" as="button" type="submit">{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="subtle">{ i18n.t("common.actions.cancel") }</fui.Button>
</fui.DialogTrigger>
</fui.DialogActions>
</fui.DialogBody>
</form>
</fui.DialogSurface>
);
}
@@ -68,12 +68,7 @@ export const useStyles_GroupView = makeStyles({
{
display: "flex",
gap: tokens.spacingHorizontalS,
visibility: "hidden",
"@media (pointer: coarse)":
{
visibility: "visible"
}
visibility: "hidden"
},
showToolbar:
{
@@ -104,12 +99,23 @@ export const useStyles_GroupView = makeStyles({
display: "flex",
columnGap: tokens.spacingHorizontalS,
rowGap: tokens.spacingHorizontalSNudge,
height: "100%"
height: "100%",
position: "relative"
},
verticalList:
{
flexFlow: "column"
},
verticalListCollapsed:
{
maxHeight: "136px",
overflow: "clip"
},
horizontalListCollapsed:
{
maxWidth: "400px",
overflow: "clip"
},
listContainer:
{
padding: `${tokens.spacingVerticalS} ${tokens.spacingHorizontalXS}`,
@@ -88,7 +88,13 @@ export default function GroupView({ group, indices, dragOverlay }: GroupViewProp
<Caption1Strong>{ i18n.t("groups.empty") }</Caption1Strong>
</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
items={ group.items.map((_, index) => [...indices, index].join("/")) }
disabled={ disableSorting }
@@ -8,6 +8,7 @@ export const useStyles_TabView = makeStyles({
width: "160px",
height: "120px",
flexShrink: 0,
marginBottom: tokens.spacingVerticalSNudge,
border: `${tokens.strokeWidthThin} solid ${tokens.colorNeutralStroke3}`,
@@ -58,8 +59,6 @@ export const useStyles_TabView = makeStyles({
display: "grid",
gridTemplateColumns: "auto 1fr auto",
alignItems: "center",
gap: tokens.spacingHorizontalSNudge,
paddingLeft: tokens.spacingHorizontalS,
borderBottomLeftRadius: tokens.borderRadiusMedium,
borderBottomRightRadius: tokens.borderRadiusMedium,
@@ -72,6 +71,9 @@ export const useStyles_TabView = makeStyles({
icon:
{
cursor: "grab",
padding: `${tokens.spacingVerticalSNudge} ${tokens.spacingHorizontalSNudge}`,
height: "32px",
boxSizing: "border-box",
"&:active":
{
@@ -86,12 +88,7 @@ export const useStyles_TabView = makeStyles({
},
deleteButton:
{
display: "none",
"@media (pointer: coarse)":
{
display: "inline-flex"
}
display: "none"
},
showDeleteButton:
{
+6 -3
View File
@@ -9,10 +9,12 @@ import { Button, Caption1, Link, mergeClasses, Tooltip } from "@fluentui/react-c
import { Dismiss20Regular } from "@fluentui/react-icons";
import { MouseEventHandler, ReactElement } from "react";
import { useStyles_TabView } from "./TabView.styles";
import CollectionContext, { CollectionContextType } from "../contexts/CollectionContext";
export default function TabView({ tab, indices, dragOverlay }: TabViewProps): ReactElement
{
const { removeItem, graphics, tilesView } = useCollections();
const { collection } = useContext<CollectionContextType>(CollectionContext);
const {
setNodeRef, setActivatorNodeRef,
nodeProps, activatorProps, isBeingDragged
@@ -29,16 +31,18 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
args.preventDefault();
args.stopPropagation();
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
if (deletePrompt)
dialog.pushPrompt({
title: i18n.t("tabs.delete"),
content: i18n.t("common.delete_prompt"),
destructive: true,
confirmText: i18n.t("common.actions.delete"),
onConfirm: () => removeItem(...indices)
onConfirm: () => removeItem(...removeIndex)
});
else
removeItem(...indices);
removeItem(...removeIndex);
};
const handleClick: MouseEventHandler<HTMLAnchorElement> = (args) =>
@@ -79,7 +83,6 @@ export default function TabView({ tab, indices, dragOverlay }: TabViewProps): Re
ref={ setActivatorNodeRef } { ...activatorProps }
src={ graphics[tab.url]?.icon ?? faviconPlaceholder }
onError={ e => e.currentTarget.src = faviconPlaceholder }
height={ 20 } width={ 20 }
className={ cls.icon } draggable={ false } />
<Tooltip relationship="description" content={ tab.title ?? tab.url }>
@@ -1,5 +1,4 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels";
import { Button, Caption1, makeStyles, mergeClasses, Subtitle2, tokens, Tooltip } from "@fluentui/react-components";
@@ -8,19 +7,32 @@ import CollectionContext, { CollectionContextType } from "../../contexts/Collect
import { useCollections } from "../../contexts/CollectionsProvider";
import CollectionMoreButton from "./CollectionMoreButton";
import OpenCollectionButton from "./OpenCollectionButton";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
export default function CollectionHeader({ dragHandleRef, dragHandleProps }: CollectionHeaderProps): React.ReactElement
{
const [contextOpen, setContextOpen] = useState<boolean>(false);
const [listLocation] = useSettings("listLocation");
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned";
const { updateCollection } = useCollections();
const { tabCount, collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const { tabCount, collection } = useContext<CollectionContextType>(CollectionContext);
const [alwaysShowToolbars] = useSettings("alwaysShowToolbars");
const AddIcon = bundleIcon(Add20Filled, Add20Regular);
const handleAddSelected = async () =>
{
const newTabs: TabItem[] = await getSelectedTabs();
updateCollection({ ...collection, items: [...collection.items, ...newTabs] }, collectionIndex);
const [newTabs, skipCount] = await getTabsToSaveAsync();
if (newTabs.length > 0)
await updateCollection({
...collection,
items: [...collection.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
}, collection.timestamp);
if (skipCount > 0)
await sendPartialSaveNotification();
};
const cls = useStyles();
@@ -48,18 +60,18 @@ export default function CollectionHeader({ dragHandleRef, dragHandleProps }: Col
mergeClasses(
cls.toolbar,
"CollectionView__toolbar",
alwaysShowToolbars === true && cls.showToolbar
(alwaysShowToolbars === true || contextOpen) && cls.showToolbar
) }
>
{ tabCount < 1 ?
<Button icon={ <AddIcon /> } appearance="subtle" onClick={ handleAddSelected }>
{ i18n.t("collections.menu.add_selected") }
{ isTabView ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
</Button>
:
<OpenCollectionButton />
<OpenCollectionButton onOpenChange={ (_, e) => setContextOpen(e.open) } />
}
<CollectionMoreButton onAddSelected={ handleAddSelected } />
<CollectionMoreButton onAddSelected={ handleAddSelected } onOpenChange={ (_, e) => setContextOpen(e.open) } />
</div>
</div>
);
@@ -95,12 +107,7 @@ const useStyles = makeStyles({
{
display: "none",
gap: tokens.spacingHorizontalS,
alignItems: "flex-start",
"@media (pointer: coarse)":
{
display: "flex"
}
alignItems: "flex-start"
},
showToolbar:
{
@@ -1,17 +1,19 @@
import { useDialog } from "@/contexts/DialogProvider";
import { useDangerStyles } from "@/hooks/useDangerStyles";
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 CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import exportCollectionToBookmarks from "../../utils/exportCollectionToBookmarks";
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 isTab: boolean = listLocation === "tab" || listLocation === "pinned";
const { removeItem, updateCollection } = useCollections();
const { tabCount, hasPinnedGroup, collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const { tabCount, hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
const dialog = useDialog();
const [deletePrompt] = useSettings("deletePrompt");
@@ -19,7 +21,6 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
const GroupIcon = ic.bundleIcon(ic.GroupList20Filled, ic.GroupList20Regular);
const EditIcon = ic.bundleIcon(ic.Edit20Filled, ic.Edit20Regular);
const DeleteIcon = ic.bundleIcon(ic.Delete20Filled, ic.Delete20Regular);
const PinnedIcon = ic.bundleIcon(ic.Pin20Filled, ic.Pin20Regular);
const BookmarkIcon = ic.bundleIcon(ic.BookmarkAdd20Filled, ic.BookmarkAdd20Regular);
const dangerCls = useDangerStyles();
@@ -32,10 +33,10 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
content: i18n.t("common.delete_prompt"),
destructive: true,
confirmText: i18n.t("common.actions.delete"),
onConfirm: () => removeItem(collectionIndex)
onConfirm: () => removeItem(collection.timestamp)
});
else
removeItem(collectionIndex);
removeItem(collection.timestamp);
};
const handleEdit = () =>
@@ -43,7 +44,7 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
<EditDialog
type="collection"
collection={ collection }
onSave={ item => updateCollection(item, collectionIndex) } />
onSave={ item => updateCollection(item, collection.timestamp) } />
);
const handleCreateGroup = () =>
@@ -51,22 +52,11 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
<EditDialog
type="group"
hidePinned={ hasPinnedGroup }
onSave={ group => updateCollection({ ...collection, items: [...collection.items, group] }, collectionIndex) } />
onSave={ group => updateCollection({ ...collection, items: [...collection.items, group] }, collection.timestamp) } />
);
const handleAddPinnedGroup = () =>
{
updateCollection({
...collection,
items: [
{ type: "group", pinned: true, items: [] },
...collection.items
]
}, collectionIndex);
};
return (
<Menu>
<Menu onOpenChange={ onOpenChange }>
<Tooltip relationship="label" content={ i18n.t("common.tooltips.more") }>
<MenuTrigger>
<Button appearance="subtle" icon={ <ic.MoreVertical20Regular /> } />
@@ -77,19 +67,12 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
<MenuList>
{ tabCount > 0 &&
<MenuItem icon={ <AddIcon /> } onClick={ () => onAddSelected?.() }>
{ i18n.t("collections.menu.add_selected") }
</MenuItem>
}
{ !import.meta.env.FIREFOX &&
<MenuItem icon={ <GroupIcon /> } onClick={ handleCreateGroup }>
{ i18n.t("collections.menu.add_group") }
</MenuItem>
}
{ (import.meta.env.FIREFOX && !hasPinnedGroup) &&
<MenuItem icon={ <PinnedIcon /> } onClick={ handleAddPinnedGroup }>
{ i18n.t("collections.menu.add_pinned") }
{ isTab ? i18n.t("collections.menu.add_all") : i18n.t("collections.menu.add_selected") }
</MenuItem>
}
<MenuItem icon={ <GroupIcon /> } onClick={ handleCreateGroup }>
{ i18n.t("collections.menu.add_group") }
</MenuItem>
{ tabCount > 0 &&
<MenuItem icon={ <BookmarkIcon /> } onClick={ () => exportCollectionToBookmarks(collection) }>
{ i18n.t("collections.menu.export_bookmarks") }
@@ -111,4 +94,5 @@ export default function CollectionMoreButton({ onAddSelected }: CollectionMoreBu
export type CollectionMoreButtonProps =
{
onAddSelected?: () => void;
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
};
@@ -3,19 +3,23 @@ import EditDialog from "@/entrypoints/sidepanel/components/EditDialog";
import CollectionContext, { CollectionContextType } from "@/entrypoints/sidepanel/contexts/CollectionContext";
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
import GroupContext, { GroupContextType } from "@/entrypoints/sidepanel/contexts/GroupContext";
import getSelectedTabs from "@/entrypoints/sidepanel/utils/getSelectedTabs";
import { useDangerStyles } from "@/hooks/useDangerStyles";
import useSettings from "@/hooks/useSettings";
import { TabItem } from "@/models/CollectionModels";
import { sendMessage } from "@/utils/messaging";
import { Button, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, Tooltip } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
import { ReactElement } from "react";
import { openGroup } from "../../utils/opener";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
export default function GroupMoreMenu(): ReactElement
{
const [listLocation] = useSettings("listLocation");
const isTabView: boolean = listLocation === "tab" || listLocation === "pinned";
const { group, indices } = useContext<GroupContextType>(GroupContext);
const { hasPinnedGroup } = useContext<CollectionContextType>(CollectionContext);
const { hasPinnedGroup, collection } = useContext<CollectionContextType>(CollectionContext);
const [deletePrompt] = useSettings("deletePrompt");
const dialog = useDialog();
const { updateGroup, removeItem, ungroup } = useCollections();
@@ -30,16 +34,18 @@ export default function GroupMoreMenu(): ReactElement
const handleDelete = () =>
{
const removeIndex: number[] = [collection.timestamp, ...indices.slice(1)];
if (deletePrompt)
dialog.pushPrompt({
title: i18n.t("groups.menu.delete"),
content: i18n.t("common.delete_prompt"),
confirmText: i18n.t("common.actions.delete"),
destructive: true,
onConfirm: () => removeItem(...indices)
onConfirm: () => removeItem(...removeIndex)
});
else
removeItem(...indices);
removeItem(...removeIndex);
};
const handleEdit = () =>
@@ -48,13 +54,29 @@ export default function GroupMoreMenu(): ReactElement
type="group"
group={ group }
hidePinned={ hasPinnedGroup }
onSave={ item => updateGroup(item, indices[0], 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 newTabs: TabItem[] = await getSelectedTabs();
updateGroup({ ...group, items: [...group.items, ...newTabs] }, indices[0], indices[1]);
const [newTabs, skipCount] = await getTabsToSaveAsync();
if (newTabs.length > 0)
await updateGroup({
...group,
items: [...group.items, ...newTabs.map<TabItem>(i => ({ type: "tab", url: i.url!, title: i.title }))]
}, collection.timestamp, indices[1]);
if (skipCount > 0)
await sendPartialSaveNotification();
};
return (
@@ -68,25 +90,23 @@ export default function GroupMoreMenu(): ReactElement
<MenuPopover>
<MenuList>
{ group.items.length > 0 &&
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => openGroup(group, true) }>
<MenuItem icon={ <NewWindowIcon /> } onClick={ openGroupInNewWindow }>
{ i18n.t("groups.menu.new_window") }
</MenuItem>
}
<MenuItem icon={ <AddIcon /> } onClick={ handleAddSelected }>
{ i18n.t("groups.menu.add_selected") }
{ isTabView ? i18n.t("groups.menu.add_all") : i18n.t("groups.menu.add_selected") }
</MenuItem>
{ (!import.meta.env.FIREFOX || group.pinned !== true) &&
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
{ i18n.t("groups.menu.edit") }
</MenuItem>
}
<MenuItem icon={ <EditIcon /> } onClick={ handleEdit }>
{ i18n.t("groups.menu.edit") }
</MenuItem>
{ group.items.length > 0 &&
<MenuItem
className={ dangerCls.menuItem }
icon={ <UngroupIcon /> }
onClick={ () => ungroup(indices[0], indices[1]) }
onClick={ () => ungroup(collection.timestamp, indices[1]) }
>
{ i18n.t("groups.menu.ungroup") }
</MenuItem>
@@ -1,18 +1,20 @@
import { useDialog } from "@/contexts/DialogProvider";
import useSettings from "@/hooks/useSettings";
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 CollectionContext, { CollectionContextType } from "../../contexts/CollectionContext";
import { useCollections } from "../../contexts/CollectionsProvider";
import { openCollection } from "../../utils/opener";
export default function OpenCollectionButton(): React.ReactElement
export default function OpenCollectionButton({ onOpenChange }: OpenCollectionButtonProps): React.ReactElement
{
const [defaultAction] = useSettings("defaultRestoreAction");
const [listLocation] = useSettings("listLocation");
const { removeItem } = useCollections();
const dialog = useDialog();
const { collection, collectionIndex } = useContext<CollectionContextType>(CollectionContext);
const { collection } = useContext<CollectionContextType>(CollectionContext);
const OpenIcon = ic.bundleIcon(ic.Open20Filled, ic.Open20Regular);
const RestoreIcon = ic.bundleIcon(ic.ArrowExportRtl20Filled, ic.ArrowExportRtl20Regular);
@@ -22,7 +24,12 @@ export default function OpenCollectionButton(): React.ReactElement
const handleIncognito = async () =>
{
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
dialog.pushPrompt({
title: i18n.t("collections.incognito_check.title"),
@@ -45,16 +52,18 @@ export default function OpenCollectionButton(): React.ReactElement
};
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 () =>
{
await openCollection(collection);
removeItem(collectionIndex);
removeItem(collection.timestamp);
};
return (
<Menu>
<Menu onOpenChange={ onOpenChange }>
<MenuTrigger disableButtonEnhancement>
{ (triggerProps: MenuButtonProps) => defaultAction === "restore" ?
<SplitButton
@@ -84,7 +93,7 @@ export default function OpenCollectionButton(): React.ReactElement
{ i18n.t("collections.actions.restore") }
</MenuItem>
}
<MenuItem icon={ <NewWindowIcon /> } onClick={ () => handleOpen("new") }>
<MenuItem icon={ <NewWindowIcon /> } onClick={ handleOpen("new") }>
{ i18n.t("collections.actions.new_window") }
</MenuItem>
<MenuItem icon={ <InPrivateIcon /> } onClick={ handleIncognito }>
@@ -95,3 +104,8 @@ export default function OpenCollectionButton(): React.ReactElement
</Menu>
);
}
export type OpenCollectionButtonProps =
{
onOpenChange?: (e: MenuOpenEvent, data: MenuOpenChangeData) => void;
};
@@ -8,7 +8,6 @@ export default CollectionContext;
export type CollectionContextType =
{
collection: CollectionItem;
collectionIndex: number;
tabCount: number;
hasPinnedGroup: boolean;
};
@@ -2,7 +2,7 @@ import { CloudStorageIssueType, getCollections, graphics as graphicsStorage, sav
import useSettings from "@/hooks/useSettings";
import { CollectionItem, GraphicsStorage, GroupItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import { onMessage } from "@/utils/messaging";
import { onMessage, sendMessage } from "@/utils/messaging";
import { createContext } from "react";
import mergePinnedGroups from "../utils/mergePinnedGroups";
@@ -40,47 +40,53 @@ export default function CollectionsProvider({ children }: React.PropsWithChildre
setCollections([...collectionList]);
await saveCollections(collectionList, cloudIssue === null);
setGraphics(await graphicsStorage.getValue());
sendMessage("refreshCollections", undefined);
};
const addCollection = (collection: CollectionItem): void =>
const addCollection = async (collection: CollectionItem): Promise<void> =>
{
updateStorage([collection, ...collections]);
await updateStorage([collection, ...collections]);
};
const removeItem = (...indices: number[]): void =>
const removeItem = async (...indices: number[]): Promise<void> =>
{
const collectionIndex: number = collections.findIndex(i => i.timestamp === indices[0]);
if (indices.length > 2)
(collections[indices[0]].items[indices[1]] as GroupItem).items.splice(indices[2], 1);
(collections[collectionIndex].items[indices[1]] as GroupItem).items.splice(indices[2], 1);
else if (indices.length > 1)
collections[indices[0]].items.splice(indices[1], 1);
collections[collectionIndex].items.splice(indices[1], 1);
else
collections.splice(indices[0], 1);
collections.splice(collectionIndex, 1);
updateStorage(collections);
await updateStorage(collections);
};
const updateCollections = (collectionList: CollectionItem[]): void =>
const updateCollections = async (collectionList: CollectionItem[]): Promise<void> =>
{
updateStorage(collectionList);
await updateStorage(collectionList);
};
const updateCollection = (collection: CollectionItem, index: number): void =>
const updateCollection = async (collection: CollectionItem, id: number): Promise<void> =>
{
const index: number = collections.findIndex(i => i.timestamp === id);
collections[index] = collection;
updateStorage(collections);
await updateStorage(collections);
};
const updateGroup = (group: GroupItem, collectionIndex: number, groupIndex: number): void =>
const updateGroup = async (group: GroupItem, collectionId: number, groupIndex: number): Promise<void> =>
{
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
collections[collectionIndex].items[groupIndex] = group;
updateStorage(collections);
await updateStorage(collections);
};
const ungroup = (collectionIndex: number, groupIndex: number): void =>
const ungroup = async (collectionId: number, groupIndex: number): Promise<void> =>
{
const collectionIndex: number = collections.findIndex(i => i.timestamp === collectionId);
const group = collections[collectionIndex].items[groupIndex] as GroupItem;
collections[collectionIndex].items.splice(groupIndex, 1, ...group.items);
updateStorage(collections);
await updateStorage(collections);
};
return (
@@ -104,12 +110,12 @@ export type CollectionsContextType =
tilesView: boolean;
refreshCollections: () => Promise<void>;
addCollection: (collection: CollectionItem) => void;
addCollection: (collection: CollectionItem) => Promise<void>;
updateCollections: (collections: CollectionItem[]) => void;
updateCollection: (collection: CollectionItem, index: number) => void;
updateGroup: (group: GroupItem, collectionIndex: number, groupIndex: number) => void;
ungroup: (collectionIndex: number, groupIndex: number) => void;
updateCollections: (collections: CollectionItem[]) => Promise<void>;
updateCollection: (collection: CollectionItem, id: number) => Promise<void>;
updateGroup: (group: GroupItem, collectionId: number, groupIndex: number) => Promise<void>;
ungroup: (collectionId: number, groupIndex: number) => Promise<void>;
removeItem: (...indices: number[]) => void;
removeItem: (...indices: number[]) => Promise<void>;
};
@@ -46,6 +46,10 @@ export const useStyles_CollectionListView = makeStyles({
listView:
{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
"@media screen and (min-width: 360px)":
{
gridTemplateColumns: "repeat(auto-fit, minmax(360px, 1fr))"
}
}
});
@@ -5,6 +5,7 @@ import CloudIssueMessages from "@/entrypoints/sidepanel/layouts/collections/mess
import CtaMessage from "@/entrypoints/sidepanel/layouts/collections/messages/CtaMessage";
import filterCollections, { CollectionFilterType } from "@/entrypoints/sidepanel/utils/filterCollections";
import sortCollections from "@/entrypoints/sidepanel/utils/sortCollections";
import { track } from "@/features/analytics";
import useSettings from "@/hooks/useSettings";
import { CollectionItem } from "@/models/CollectionModels";
import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
@@ -20,6 +21,7 @@ import { collisionDetector } from "../../utils/dnd/collisionDetector";
import { useStyles_CollectionListView } from "./CollectionListView.styles";
import SearchBar from "./SearchBar";
import StorageCapacityIssueMessage from "./messages/StorageCapacityIssueMessage";
import { snapHandleToCursor } from "../../utils/dnd/snapHandleToCursor";
export default function CollectionListView(): ReactElement
{
@@ -32,7 +34,7 @@ export default function CollectionListView(): ReactElement
const [active, setActive] = useState<DndItem | null>(null);
const sensors = useSensors(
useSensor(MouseSensor, { activationConstraint: { delay: 100, tolerance: 0 } }),
useSensor(MouseSensor, { activationConstraint: { delay: 10, tolerance: 20 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 300, tolerance: 20 } })
);
@@ -64,6 +66,8 @@ export default function CollectionListView(): ReactElement
updateCollections(result);
if (sortMode !== "custom")
setSortMode("custom");
track("used_drag_and_drop");
}
};
@@ -107,6 +111,7 @@ export default function CollectionListView(): ReactElement
collisionDetection={ collisionDetector(!tilesView) }
onDragStart={ handleDragStart }
onDragEnd={ handleDragEnd }
modifiers={ [snapHandleToCursor] }
>
<SortableContext
items={ resultList.map((_, index) => index.toString()) }
@@ -118,27 +123,25 @@ export default function CollectionListView(): ReactElement
</SortableContext>
<DragOverlay dropAnimation={ null }>
{ active &&
<>
{ active.item.type === "collection" &&
<CollectionView collection={ active.item } index={ -1 } dragOverlay />
}
{ active.item.type === "group" &&
<CollectionContext.Provider
value={ {
tabCount: 0,
collectionIndex: active.indices[0],
collection: resultList[active.indices[0]],
hasPinnedGroup: true
} }
>
{ active !== null ?
active.item.type === "collection" ?
<CollectionView collection={ active.item } index={ -1 } dragOverlay />
:
<CollectionContext.Provider
value={ {
tabCount: 0,
collection: resultList[active.indices[0]],
hasPinnedGroup: true
} }
>
{ active.item.type === "group" ?
<GroupView group={ active.item } indices={ [-1] } dragOverlay />
</CollectionContext.Provider>
}
{ active.item.type === "tab" &&
<TabView tab={ active.item } indices={ [-1] } dragOverlay />
}
</>
:
<TabView tab={ active.item } indices={ [-1] } dragOverlay />
}
</CollectionContext.Provider>
:
<></>
}
</DragOverlay>
</DndContext>
@@ -44,11 +44,11 @@ export default function FilterCollectionsButton({ value, onChange }: FilterColle
<ColorIcon
className={ fui.mergeClasses(
cls.colorIcon,
colorCls[i as chrome.tabGroups.ColorEnum]
colorCls[i as `${Browser.tabGroups.Color}`]
) } />
}
>
{ i18n.t(`colors.${i as chrome.tabGroups.ColorEnum}`) }
{ i18n.t(`colors.${i as `${Browser.tabGroups.Color}`}`) }
</fui.MenuItemCheckbox>
) }
</fui.MenuList>
@@ -1,7 +1,8 @@
import resolveConflict from "@/features/collectionStorage/utils/resolveConflict";
import { Button, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
import { ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons";
import { ArrowDownload20Regular, ArrowUpload20Regular, CloudArrowDown20Regular, Wrench20Regular } from "@fluentui/react-icons";
import { useCollections } from "../../../contexts/CollectionsProvider";
import exportData from "@/entrypoints/options/utils/exportData";
export default function CloudIssueMessages(props: MessageBarProps): React.ReactElement
{
@@ -36,6 +37,9 @@ export default function CloudIssueMessages(props: MessageBarProps): React.ReactE
{ i18n.t("merge_conflict_message.message") }
</MessageBarBody>
<MessageBarActions>
<Button icon={ <ArrowDownload20Regular /> } onClick={ exportData }>
{ i18n.t("options_page.storage.export") }
</Button>
<Button icon={ <ArrowUpload20Regular /> } onClick={ () => overrideStorageWith("local") }>
{ i18n.t("merge_conflict_message.accept_local") }
</Button>
@@ -1,5 +1,6 @@
import { BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
import { buyMeACoffeeLink, storeLink } from "@/data/links";
import { track } from "@/features/analytics";
import { useBmcStyles } from "@/hooks/useBmcStyles";
import extLink from "@/utils/extLink";
import { Button, Link, MessageBar, MessageBarActions, MessageBarBody, MessageBarProps, MessageBarTitle } from "@fluentui/react-components";
@@ -27,6 +28,11 @@ export default function CtaMessage(props: MessageBarProps): ReactElement
{
await ctaCounter.setValue(counter);
setCounter(counter);
if (counter === -1)
track("bmc_clicked");
else
track("cta_dismissed");
};
if (counter < 50)
@@ -36,7 +42,7 @@ export default function CtaMessage(props: MessageBarProps): ReactElement
<MessageBar layout="multiline" icon={ <HeartFilled color="red" /> } { ...props }>
<MessageBarBody>
<MessageBarTitle>{ i18n.t("cta_message.title") }</MessageBarTitle>
{ i18n.t("cta_message.message") } <Link { ...extLink(storeLink) }>{ i18n.t("cta_message.feedback") }</Link>
{ i18n.t("cta_message.message") } <Link { ...extLink(storeLink) } onClick={ () => track("feedback_clicked") }>{ i18n.t("cta_message.feedback") }</Link>
</MessageBarBody>
<MessageBarActions
containerAction={
@@ -1,7 +1,7 @@
import useStorageInfo from "@/hooks/useStorageInfo";
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();
@@ -1,6 +1,11 @@
import { useCollections } from "@/entrypoints/sidepanel/contexts/CollectionsProvider";
import { track } from "@/features/analytics";
import useSettings, { SettingsValue } from "@/hooks/useSettings";
import saveTabsToCollection from "@/utils/saveTabsToCollection";
import { CollectionItem } from "@/models/CollectionModels";
import { closeTabsAsync } from "@/utils/closeTabsAsync";
import { createCollectionFromTabs } from "@/utils/createCollectionFromTabs";
import { getTabsToSaveAsync } from "@/utils/getTabsToSaveAsync";
import sendPartialSaveNotification from "@/utils/sendPartialSaveNotification";
import watchTabSelection from "@/utils/watchTabSelection";
import { Menu, MenuButtonProps, MenuItem, MenuList, MenuPopover, MenuTrigger, SplitButton } from "@fluentui/react-components";
import * as ic from "@fluentui/react-icons";
@@ -14,8 +19,26 @@ export default function ActionButton(): ReactElement
const handleAction = async (primary: boolean) =>
{
const colection = await saveTabsToCollection(primary === (defaultAction === "set_aside"));
addCollection(colection);
const [tabs, skipCount] = await getTabsToSaveAsync();
if (tabs.length < 1)
{
await sendPartialSaveNotification();
return;
}
const collection: CollectionItem = await createCollectionFromTabs(tabs);
await addCollection(collection);
if (skipCount > 0)
await sendPartialSaveNotification();
const closeTabs: boolean = primary === (defaultAction === "set_aside");
if (closeTabs)
await closeTabsAsync(tabs);
track(closeTabs ? "set_aside" : "save");
};
useEffect(() =>
@@ -1,5 +1,6 @@
import { BuyMeACoffee20Filled, BuyMeACoffee20Regular } from "@/assets/BuyMeACoffee20";
import { buyMeACoffeeLink, githubLinks, storeLink } from "@/data/links";
import { track } from "@/features/analytics";
import useSettings from "@/hooks/useSettings";
import extLink from "@/utils/extLink";
import sendNotification from "@/utils/sendNotification";
@@ -41,10 +42,10 @@ export default function MoreButton(): ReactElement
<fui.MenuDivider />
<fui.MenuItemLink icon={ <BmcIcon /> } { ...extLink(buyMeACoffeeLink) }>
<fui.MenuItemLink icon={ <BmcIcon /> } { ...extLink(buyMeACoffeeLink) } onClick={ () => track("feedback_clicked") }>
{ i18n.t("common.cta.sponsor") }
</fui.MenuItemLink>
<fui.MenuItemLink icon={ <FeedbackIcon /> } { ...extLink(storeLink) } >
<fui.MenuItemLink icon={ <FeedbackIcon /> } { ...extLink(storeLink) } onClick={ () => track("bmc_clicked") }>
{ i18n.t("common.cta.feedback") }
</fui.MenuItemLink>
<fui.MenuItemLink icon={ <LearnIcon /> } { ...extLink(githubLinks.release) } >
+8 -1
View File
@@ -1,5 +1,6 @@
import App from "@/App.tsx";
import "@/assets/global.css";
import { trackPage } from "@/features/analytics";
import { useLocalMigration } from "@/features/migration";
import useWelcomeDialog from "@/features/v3welcome/hooks/useWelcomeDialog";
import { Divider, makeStyles } from "@fluentui/react-components";
@@ -7,6 +8,8 @@ import ReactDOM from "react-dom/client";
import CollectionsProvider from "./contexts/CollectionsProvider";
import CollectionListView from "./layouts/collections/CollectionListView";
import Header from "./layouts/header/Header";
import { useSettingsReviewDialog } from "@/features/settingsReview";
import useDialogTrain from "@/hooks/useDialogTrain";
ReactDOM.createRoot(document.getElementById("root")!).render(
<App>
@@ -15,13 +18,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
);
document.title = i18n.t("manifest.name");
trackPage("collection_list");
function MainPage(): React.ReactElement
{
const cls = useStyles();
useLocalMigration();
useWelcomeDialog();
useDialogTrain(
useWelcomeDialog,
useSettingsReviewDialog
);
return (
<CollectionsProvider>
@@ -33,9 +33,11 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
if (activeItem.item.type === "collection")
{
// If we drag a collection, we should ignore other items, like tabs or groups
if (droppableItem.item.type !== "collection")
continue;
// Using distance between centers
value = distanceBetween(centerOfRectangle(rect), centerRect);
collisions.push({ id, data: { droppableContainer, value } });
continue;
@@ -44,14 +46,20 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
const intersectionRatio: number = getIntersectionRatio(rect, collisionRect);
const intersectionCoefficient: number = intersectionRatio / getMaxIntersectionRatio(rect, collisionRect);
// Dragging a tab or a group over a 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])
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;
// 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])
{
const [collectionId, groupId] = activeItem.indices;
@@ -62,16 +70,23 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
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
{
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"))
{
// Ignore, if we're dragging a group
if (activeItem.item.type === "group")
continue;
// Ignore, if we're dragging a tab, that's inside the group
if (
activeItem.indices.length === 3 &&
activeItem.indices[0] === droppableItem.indices[0] &&
@@ -79,11 +94,15 @@ export function collisionDetector(vertical?: boolean): CollisionDetection
)
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)
continue;
// Use intersection between the tab and the group's dropzone
value = 1 / intersectionRatio;
}
// We're dragging a group or a tab over its sibling
else if (activeItem.indices.length === droppableItem.indices.length)
{
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])
continue;
// Ignore pinned groups
if (droppableItem.item.type === "group" && droppableItem.item.pinned === true)
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")
{
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,11 +1,21 @@
import { track } from "@/features/analytics";
import { CollectionItem, TabItem } from "@/models/CollectionModels";
import sendNotification from "@/utils/sendNotification";
import { Bookmarks } from "wxt/browser";
import { getCollectionTitle } from "./getCollectionTitle";
export default async function exportCollectionToBookmarks(collection: CollectionItem)
{
const rootFolder: Bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
const permissions: Browser.permissions.Permissions = await browser.permissions.getAll();
if (!permissions.permissions?.includes("bookmarks"))
{
const granted: boolean = await browser.permissions.request({ permissions: ["bookmarks"] });
if (!granted)
return;
}
const rootFolder: Browser.bookmarks.BookmarkTreeNode = await browser.bookmarks.create({
title: getCollectionTitle(collection)
});
@@ -31,6 +41,8 @@ export default async function exportCollectionToBookmarks(collection: Collection
}
}
track("bookmarks_saved");
await sendNotification({
title: i18n.t("notifications.bookmark_saved.title"),
message: i18n.t("notifications.bookmark_saved.message"),
@@ -61,5 +61,5 @@ export default function filterCollections(
export type CollectionFilterType =
{
query: string;
colors: (chrome.tabGroups.ColorEnum | "none")[];
colors: (`${Browser.tabGroups.Color}` | "none")[];
};
@@ -1,8 +1,10 @@
import { CollectionItem } from "@/models/CollectionModels";
export function getCollectionTitle(collection?: CollectionItem): string
export function getCollectionTitle(collection?: CollectionItem, useTimestamp?: boolean): string
{
return collection?.title
|| new Date(collection?.timestamp ?? Date.now())
.toLocaleDateString(browser.i18n.getUILanguage(), { year: "numeric", month: "short", day: "numeric" });
if (collection?.title !== undefined && useTimestamp !== true)
return collection.title;
return new Date(collection?.timestamp ?? Date.now())
.toLocaleDateString(browser.i18n.getUILanguage(), { year: "numeric", month: "short", day: "numeric" });
}
+18 -3
View File
@@ -1,8 +1,23 @@
import { TabItem } from "@/models/CollectionModels";
import { Tabs } from "wxt/browser";
import sendNotification from "@/utils/sendNotification";
export default async function getSelectedTabs(): Promise<TabItem[]>
{
const 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 }));
let tabs: Browser.tabs.Tab[] = await browser.tabs.query({ currentWindow: true, highlighted: true });
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 }));
}
+14 -15
View File
@@ -1,7 +1,6 @@
import { getCollectionTitle } from "@/entrypoints/sidepanel/utils/getCollectionTitle";
import { CollectionItem, GroupItem, TabItem } from "@/models/CollectionModels";
import { settings } from "@/utils/settings";
import { Tabs, Windows } from "wxt/browser";
export async function openCollection(collection: CollectionItem, targetWindow?: "current" | "new" | "incognito"): Promise<void>
{
@@ -55,42 +54,42 @@ export async function openGroup(group: GroupItem, newWindow: boolean = false): P
async function createGroup(group: GroupItem, windowId: number, discard?: boolean): Promise<void>
{
discard ??= await settings.dismissOnLoad.getValue();
const tabIds: number[] = await Promise.all(group.items.map(async i =>
(await createTab(i.url, windowId, discard, group.pinned)).id!
const tabs: Browser.tabs.Tab[] = await Promise.all(group.items.map(async i =>
await createTab(i.url, windowId, discard, group.pinned)
));
// "Pinned" group is technically not a group, so not much else to do here
// and Firefox doesn't even support tab groups
if (group.pinned === true || import.meta.env.FIREFOX)
if (group.pinned === true)
return;
const groupId: number = await chrome.tabs.group({
tabIds, createProperties: {
windowId
}
const groupId: number = await browser.tabs.group({
tabIds: tabs.filter(i => i.windowId === windowId).map(i => i.id!) as [number, ...number[]],
createProperties: { windowId }
});
await chrome.tabGroups.update(groupId, {
await browser.tabGroups.update(groupId, {
title: group.title,
color: group.color
});
}
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Windows.CreateCreateDataType): Promise<void>
async function manageWindow(handle: (windowId: number) => Promise<void>, windowProps?: Browser.windows.CreateData): Promise<void>
{
const currentWindow: Windows.Window = windowProps ?
await browser.windows.create({ url: "about:blank", focused: true, ...windowProps }) :
const currentWindow: Browser.windows.Window = windowProps ?
(await browser.windows.create({ url: "about:blank", focused: false, ...windowProps }))! :
await browser.windows.getCurrent();
const windowId: number = currentWindow.id!;
await handle(windowId);
await browser.windows.update(windowId, { focused: true });
if (windowProps)
// Close "about:blank" tab
await browser.tabs.remove(currentWindow.tabs![0].id!);
}
async function createTab(url: string, windowId: number, discard: boolean, pinned?: boolean): Promise<Tabs.Tab>
async function createTab(url: string, windowId: number, discard: boolean, pinned?: boolean): Promise<Browser.tabs.Tab>
{
const tab = await browser.tabs.create({ url, windowId: windowId, active: false, pinned });
@@ -102,7 +101,7 @@ async function createTab(url: string, windowId: number, discard: boolean, pinned
function discardOnLoad(tabId: number): void
{
const handleTabUpdated = (id: number, _: any, tab: Tabs.Tab) =>
const handleTabUpdated = (id: number, _: any, tab: Browser.tabs.Tab) =>
{
if (id !== tabId || !tab.url)
return;
+3 -2
View File
@@ -50,7 +50,7 @@ export default defineConfig([
"@stylistic/semi": ["error", "always"],
"@stylistic/block-spacing": ["warn", "always"],
"@stylistic/arrow-spacing": ["warn", { before: true, after: true }],
"@stylistic/indent": ["warn", "tab"],
"@stylistic/indent": ["warn", "tab", { assignmentOperator: "off" }],
"@stylistic/quotes": ["error", "double"],
"@stylistic/comma-spacing": ["warn"],
"@stylistic/comma-dangle": ["warn", "never"],
@@ -88,7 +88,8 @@ export default defineConfig([
"@typescript-eslint/no-unused-vars": ["warn"],
"prefer-const": ["warn"],
"@stylistic/padded-blocks": ["warn"],
"no-empty": ["off"]
"no-empty": ["off"],
"@stylistic/eol-last": ["warn"]
}
},
{
+55
View File
@@ -0,0 +1,55 @@
import { analytics } from "./utils/analytics";
import analyticsPermission from "./utils/analyticsPermission";
import { getUserProperties, userId } from "./utils/getUserProperties";
export { analyticsPermission };
export async function track(eventName: string, eventProperties?: Record<string, string>): Promise<void>
{
try
{
if (!await analyticsPermission.getValue())
return;
analytics.track(eventName, eventProperties);
}
catch (ex)
{
console.error("Failed to send analytics event", ex);
}
}
export async function trackError(name: string, error: Error): Promise<void>
{
try
{
if (!await analyticsPermission.getValue())
return;
analytics.track(name, {
name: error.name,
message: error.message,
stack: error.stack ?? "no_stack"
});
}
catch (ex)
{
console.error("Failed to send error report", ex);
}
}
export async function trackPage(pageName: string): Promise<void>
{
try
{
if (!await analyticsPermission.getValue())
return;
analytics.identify(await userId.getValue() as string, await getUserProperties());
analytics.page(pageName);
}
catch (ex)
{
console.error("Failed to send page view", ex);
}
}
+12
View File
@@ -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,74 @@
import { Unwatch, WatchCallback } from "wxt/utils/storage";
import { analytics } from "./analytics";
const analyticsPermission: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
{
getValue: async (): Promise<boolean> =>
{
const isGranted: boolean = import.meta.env.FIREFOX
? await browser.permissions.contains({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions)
: await allowAnalytics.getValue();
analytics.setEnabled(isGranted);
return isGranted;
},
setValue: async (value: boolean) =>
{
if (!import.meta.env.FIREFOX)
{
await allowAnalytics.setValue(value);
return;
}
let result: boolean = false;
if (value)
result = await browser.permissions.request({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions);
else
result = await browser.permissions.remove({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions);
if (!result)
throw new Error("Permission request was denied");
},
watch: (cb: WatchCallback<boolean>): Unwatch =>
{
if (!import.meta.env.FIREFOX)
return allowAnalytics.watch(cb);
const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
{
// @ts-expect-error Firefox-only API
if (permissions.data_collection?.includes("technicalAndInteraction"))
{
const isGranted: boolean = await browser.permissions.contains({
data_collection: ["technicalAndInteraction"]
} as Browser.permissions.Permissions);
cb(isGranted, !isGranted);
}
};
browser.permissions.onAdded.addListener(listener);
browser.permissions.onRemoved.addListener(listener);
return (): void =>
{
browser.permissions.onAdded.removeListener(listener);
browser.permissions.onRemoved.removeListener(listener);
};
}
};
export default analyticsPermission;
const allowAnalytics = storage.defineItem<boolean>("local:analytics", {
fallback: true
});
@@ -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;
};
+3
View File
@@ -5,6 +5,9 @@ export { default as getCollections } from "./utils/getCollections";
export { default as resoveConflict } from "./utils/resolveConflict";
export { default as saveCollections } from "./utils/saveCollections";
export { default as setCloudStorage } from "./utils/setCloudStorage";
export { default as clearGraphicsStorage } from "./utils/clearGraphics";
export { default as thumbnailCaptureEnabled } from "./utils/thumbnailCaptureEnabled";
export const collectionCount = collectionStorage.count;
export const graphics = collectionStorage.graphics;
@@ -0,0 +1,6 @@
import { collectionStorage } from "./collectionStorage";
export default async function clearGraphicsStorage(): Promise<void>
{
await collectionStorage.graphics.removeValue();
}
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { CollectionItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import { collectionStorage } from "./collectionStorage";
@@ -32,6 +33,7 @@ export default async function getCollections(): Promise<[CollectionItem[], Cloud
{
logger("Failed to get cloud storage");
console.error(ex);
trackError("cloud_get_error", ex as Error);
return [await getCollectionsFromLocal(), "parse_error"];
}
}
@@ -14,7 +14,7 @@ export default async function getCollectionsFromCloud(): Promise<CollectionItem[
const chunks: Record<string, string> =
await browser.storage.sync.get(getChunkKeys(0, chunkCount)) as Record<string, string>;
const data: string = decompress(Object.values(chunks).join(), { inputEncoding: "StorageBinaryString" });
const data: string = decompress(Object.values(chunks).join(""), { inputEncoding: "Base64" });
return parseCollections(data);
}
@@ -44,7 +44,7 @@ function parseCollection(data: string): CollectionItem
return {
type: "collection",
timestamp: parseInt(data.match(/(?<=^c)\d+/)!.toString()),
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
color: data.match(/(?<=^c\d+\/)[a-z]+/)?.toString() as `${Browser.tabGroups.Color}`,
title: data.match(/(?<=^c[\da-z/]*\|).*/)?.toString(),
items: []
};
@@ -64,7 +64,7 @@ function parseGroup(data: string): GroupItem
return {
type: "group",
pinned: false,
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as chrome.tabGroups.ColorEnum,
color: data.match(/(?<=^\tg\/)[a-z]+/)?.toString() as `${Browser.tabGroups.Color}`,
title: data.match(/(?<=^\tg\/[a-z]+\|).*$/)?.toString(),
items: []
};
@@ -1,3 +1,4 @@
import { trackError } from "@/features/analytics";
import { CollectionItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import { collectionStorage } from "./collectionStorage";
@@ -37,5 +38,6 @@ async function replaceLocalWithCloud(): Promise<void>
{
logger("Failed to get cloud storage");
console.error(ex);
trackError("conflict_resolve_with_cloud_error", ex as Error);
}
}
@@ -1,6 +1,5 @@
import { CollectionItem, GraphicsStorage } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import sendNotification from "@/utils/sendNotification";
import { collectionStorage } from "./collectionStorage";
import saveCollectionsToCloud from "./saveCollectionsToCloud";
import saveCollectionsToLocal from "./saveCollectionsToLocal";
@@ -18,28 +17,8 @@ export default async function saveCollections(
await saveCollectionsToLocal(collections, timestamp);
if (updateCloud && await collectionStorage.disableCloud.getValue() !== true)
try
{
await saveCollectionsToCloud(collections, timestamp);
}
catch (ex)
{
logger("Failed to save cloud storage");
console.error(ex);
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
await sendNotification({
title: i18n.t("notifications.error_quota_exceeded.title"),
message: i18n.t("notifications.error_quota_exceeded.message"),
icon: "/notification_icons/cloud_error.png"
});
else
await sendNotification({
title: i18n.t("notifications.error_storage_full.title"),
message: i18n.t("notifications.error_storage_full.message"),
icon: "/notification_icons/cloud_error.png"
});
}
await saveCollectionsToCloud(collections, timestamp);
await updateGraphics(collections, graphicsCache);
logger("Save complete");
};
@@ -1,46 +1,75 @@
import { trackError } from "@/features/analytics";
import { CollectionItem } from "@/models/CollectionModels";
import getLogger from "@/utils/getLogger";
import sendNotification from "@/utils/sendNotification";
import { compress } from "lzutf8";
import { WxtStorageItem } from "wxt/storage";
import { collectionStorage } from "./collectionStorage";
import getChunkKeys from "./getChunkKeys";
import serializeCollections from "./serializeCollections";
const logger = getLogger("saveCollectionsToCloud");
export default async function saveCollectionsToCloud(collections: CollectionItem[], timestamp: number): Promise<void>
{
if (!collections || collections.length < 1)
try
{
await collectionStorage.chunkCount.setValue(0);
await browser.storage.sync.remove(getChunkKeys());
return;
if (!collections || collections.length < 1)
{
await browser.storage.sync.set({
[getStorageKey(collectionStorage.chunkCount)]: 0,
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
});
await browser.storage.sync.remove(getChunkKeys());
return;
}
const data: string = compress(serializeCollections(collections), { outputEncoding: "Base64" });
const chunks: string[] = splitIntoChunks(data);
if (chunks.length > collectionStorage.maxChunkCount)
throw new Error("Data is too large to be stored in sync storage.");
// Since there's a limit for cloud write operations, we need to write all chunks in one go.
const newRecords: Record<string, string | number> =
{
[getStorageKey(collectionStorage.chunkCount)]: chunks.length,
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
};
for (let i = 0; i < chunks.length; i++)
newRecords[`c${i}`] = chunks[i];
await browser.storage.sync.set(newRecords);
if (chunks.length < collectionStorage.maxChunkCount)
await browser.storage.sync.remove(getChunkKeys(chunks.length));
}
const data: string = compress(serializeCollections(collections), { outputEncoding: "StorageBinaryString" });
const chunks: string[] = splitIntoChunks(data);
if (chunks.length > collectionStorage.maxChunkCount)
throw new Error("Data is too large to be stored in sync storage.");
// Since there's a limit for cloud write operations, we need to write all chunks in one go.
const newRecords: Record<string, string | number> =
catch (ex)
{
[getStorageKey(collectionStorage.chunkCount)]: chunks.length,
[getStorageKey(collectionStorage.syncLastUpdated)]: timestamp
};
logger("Failed to save cloud storage");
console.error(ex);
trackError("cloud_save_error", ex as Error);
for (let i = 0; i < chunks.length; i++)
newRecords[`c${i}`] = chunks[i];
await browser.storage.sync.set(newRecords);
if (chunks.length < collectionStorage.maxChunkCount)
await browser.storage.sync.remove(getChunkKeys(chunks.length));
if ((ex as Error).message.includes("MAX_WRITE_OPERATIONS_PER_MINUTE"))
await sendNotification({
title: i18n.t("notifications.error_quota_exceeded.title"),
message: i18n.t("notifications.error_quota_exceeded.message"),
icon: "/notification_icons/cloud_error.png"
});
else
await sendNotification({
title: i18n.t("notifications.error_storage_full.title"),
message: i18n.t("notifications.error_storage_full.message"),
icon: "/notification_icons/cloud_error.png"
});
}
}
function splitIntoChunks(data: string): string[]
{
// QUOTA_BYTES_PER_ITEM includes length of key name, length of content and 2 more bytes (for unknown reason).
const chunkKey: string = getChunkKeys(collectionStorage.maxChunkCount - 1)[0];
const chunkSize = (chrome.storage.sync.QUOTA_BYTES_PER_ITEM ?? 8192) - chunkKey.length - 2;
const chunkSize = (browser.storage.sync.QUOTA_BYTES_PER_ITEM ?? 8192) - chunkKey.length - 2;
const chunks: string[] = [];
for (let i = 0; i < data.length; i += chunkSize)
@@ -1,3 +1,4 @@
import { sendMessage } from "@/utils/messaging";
import { collectionStorage } from "./collectionStorage";
import saveCollectionsToCloud from "./saveCollectionsToCloud";
@@ -14,6 +15,6 @@ export default async function setCloudStorage(enable: boolean): Promise<void>
{
await collectionStorage.disableCloud.setValue(true);
await saveCollectionsToCloud([], 0);
browser.runtime.reload();
await sendMessage("refreshCollections", undefined);
}
}
@@ -0,0 +1,49 @@
import { Unwatch, WatchCallback } from "wxt/utils/storage";
const thumbnailCaptureEnabled: Pick<WxtStorageItem<boolean, Record<string, unknown>>, "getValue" | "watch" | "setValue"> =
{
getValue: async (): Promise<boolean> =>
await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] }),
watch: (cb: WatchCallback<boolean>): Unwatch =>
{
const listener = async (permissions: Browser.permissions.Permissions): Promise<void> =>
{
if (permissions.permissions?.includes("scripting") || permissions.origins?.includes("<all_urls>"))
{
const isGranted: boolean = await browser.permissions.contains({ permissions: ["scripting"], origins: ["<all_urls>"] });
console.log("thumbnailCaptureEnabled changed", isGranted);
cb(isGranted, !isGranted);
}
};
browser.permissions.onAdded.addListener(listener);
browser.permissions.onRemoved.addListener(listener);
return (): void =>
{
browser.permissions.onAdded.removeListener(listener);
browser.permissions.onRemoved.removeListener(listener);
};
},
setValue: async (value: boolean): Promise<void> =>
{
let result: boolean = false;
if (value)
result = await browser.permissions.request({ permissions: ["scripting"], origins: ["<all_urls>"] });
else
{
result = await browser.permissions.remove({ origins: ["<all_urls>"] });
if (import.meta.env.DEV)
await browser.permissions.request({ origins: ["http://localhost/*"] });
}
if (!result)
throw new Error("Permission request was denied");
}
};
export default thumbnailCaptureEnabled;
@@ -0,0 +1,132 @@
import { githubLinks } from "@/data/links";
import { analyticsPermission } from "@/features/analytics";
import { thumbnailCaptureEnabled } from "@/features/collectionStorage";
import extLink from "@/utils/extLink";
import * as fui from "@fluentui/react-components";
import { Unwatch } from "wxt/utils/storage";
import { reviewSettings } from "../utils/setSettingsReviewNeeded";
import { settingsForReview } from "../utils/showSettingsReviewDialog";
export default function SettingsReviewDialog(): React.ReactElement
{
const [allowAnalytics, setAllowAnalytics] = useState<boolean | null>(null);
const [captureThumbnails, setCaptureThumbnails] = useState<boolean | null>(null);
const [needsReview, setNeedsReview] = useState<string[]>([]);
const cls = useStyles();
useEffect(() =>
{
analyticsPermission.getValue().then(setAllowAnalytics);
thumbnailCaptureEnabled.getValue().then(setCaptureThumbnails);
settingsForReview.getValue().then(setNeedsReview);
const unwatchAnalytics: Unwatch = analyticsPermission.watch(setAllowAnalytics);
const unwatchThumbnails: Unwatch = thumbnailCaptureEnabled.watch(setCaptureThumbnails);
return () =>
{
unwatchAnalytics();
unwatchThumbnails();
};
}, []);
const updateAnalytics = (enabled: boolean): void =>
{
setAllowAnalytics(null);
analyticsPermission.setValue(enabled)
.catch(() => setAllowAnalytics(!enabled));
};
const updateThumbnails = (enabled: boolean): void =>
{
setCaptureThumbnails(null);
thumbnailCaptureEnabled.setValue(enabled)
.catch(() => setCaptureThumbnails(!enabled));
};
return (
<fui.DialogSurface>
<fui.DialogBody>
<fui.DialogTitle>{ i18n.t("features.settingsReview.title") }</fui.DialogTitle>
<fui.DialogContent className={ cls.content }>
{ needsReview.includes(reviewSettings.THUMBNAILS) &&
<div className={ cls.section }>
<fui.Switch
label={ i18n.t("options_page.storage.thumbnail_capture") }
checked={ captureThumbnails ?? true }
disabled={ captureThumbnails === null }
onChange={ (_, e) => updateThumbnails(e.checked as boolean) } />
<fui.MessageBar layout="multiline">
<fui.MessageBarBody className={ cls.msgBarBody }>
<fui.MessageBarTitle>
{ i18n.t("options_page.storage.thumbnail_capture_notice1") }
</fui.MessageBarTitle>
<fui.Text as="p">
{ i18n.t("options_page.storage.thumbnail_capture_notice2") }
</fui.Text>
</fui.MessageBarBody>
</fui.MessageBar>
</div>
}
{ needsReview.includes(reviewSettings.ANALYTICS) &&
<div className={ cls.section }>
<fui.Switch
label={ i18n.t("options_page.general.options.allow_analytics") }
checked={ allowAnalytics ?? true }
disabled={ allowAnalytics === null }
onChange={ (_, e) => updateAnalytics(e.checked as boolean) } />
<fui.MessageBar layout="multiline">
<fui.MessageBarBody className={ cls.msgBarBody }>
<fui.MessageBarTitle>
{ i18n.t("features.settingsReview.analytics.title") }
</fui.MessageBarTitle>
<fui.Text as="p">
{ i18n.t("features.settingsReview.analytics.p1") }
</fui.Text>
<fui.Text as="p" weight="semibold">
{ i18n.t("features.settingsReview.analytics.p2") }
</fui.Text>
<fui.Text as="p">
{ i18n.t("features.settingsReview.analytics.p3_text") } <fui.Link { ...extLink(githubLinks.privacy) }>{ i18n.t("features.settingsReview.analytics.p3_link") }</fui.Link>.
</fui.Text>
</fui.MessageBarBody>
</fui.MessageBar>
</div>
}
</fui.DialogContent>
<fui.DialogActions>
<fui.Button onClick={ () => browser.runtime.openOptionsPage() }>
{ i18n.t("features.settingsReview.action") }
</fui.Button>
<fui.DialogTrigger>
<fui.Button appearance="primary">{ i18n.t("common.actions.save") }</fui.Button>
</fui.DialogTrigger>
</fui.DialogActions>
</fui.DialogBody>
</fui.DialogSurface>
);
}
const useStyles = fui.makeStyles({
content:
{
display: "flex",
flexFlow: "column",
gap: fui.tokens.spacingVerticalL
},
section:
{
display: "flex",
flexFlow: "column",
gap: fui.tokens.spacingVerticalXS
},
msgBarBody:
{
display: "flex",
flexFlow: "column",
gap: fui.tokens.spacingVerticalXS,
marginBottom: fui.tokens.spacingVerticalXS
}
});
@@ -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();
});
});
}
+1
View File
@@ -0,0 +1 @@
export { default as useSettingsReviewDialog } from "./hooks/useSettingsReviewDialog";
+1
View File
@@ -0,0 +1 @@
export { default as setSettingsReviewNeeded } from "./setSettingsReviewNeeded";
@@ -0,0 +1,65 @@
import { analyticsPermission } from "@/features/analytics";
import { settingsForReview } from "./showSettingsReviewDialog";
export default async function setSettingsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<void>
{
const needsReview: string[] = await settingsForReview.getValue();
if (!needsReview.includes(reviewSettings.ANALYTICS) && await checkAnalyticsReviewNeeded(installReason, previousVersion))
needsReview.push(reviewSettings.ANALYTICS);
if (!needsReview.includes(reviewSettings.THUMBNAILS) && await checkThumbnailsReviewNeeded(installReason, previousVersion))
needsReview.push(reviewSettings.THUMBNAILS);
console.log("Settings needing review:", needsReview);
// Add more settings here as needed
if (needsReview.length > 0)
await settingsForReview.setValue(needsReview);
}
export const reviewSettings =
{
ANALYTICS: "analytics",
THUMBNAILS: "thumbnails"
};
async function checkAnalyticsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<boolean>
{
if (installReason === "install")
return !await analyticsPermission.getValue();
if (installReason === "update")
{
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
const cumulative: number = major * 10000 + minor * 100 + patch;
if (cumulative < 30100) // < 3.1.0
return true;
}
if (import.meta.env.DEV)
return true;
return false;
}
async function checkThumbnailsReviewNeeded(installReason: `${Browser.runtime.OnInstalledReason}`, previousVersion?: string): Promise<boolean>
{
if (installReason === "install")
return true;
if (installReason === "update")
{
const [major, minor, patch] = (previousVersion ?? "0.0.0").split(".").map(parseInt);
const cumulative: number = major * 10000 + minor * 100 + patch;
if (cumulative < 30100) // < 3.1.0
return true;
}
if (import.meta.env.DEV)
return true;
return false;
}
@@ -0,0 +1,6 @@
export const settingsForReview = storage.defineItem<string[]>(
"local:settingsForReview",
{
fallback: []
}
);
@@ -1,5 +1,6 @@
import { useTheme } from "@/contexts/ThemeProvider";
import { v3blogPost } from "@/data/links";
import { track } from "@/features/analytics";
import extLink from "@/utils/extLink";
import * as fui from "@fluentui/react-components";
@@ -23,9 +24,7 @@ export default function WelcomeDialog(): React.ReactElement
{ i18n.t("features.v3welcome.text2") }
</fui.Body1>
<ul>
{ !import.meta.env.FIREFOX &&
<li>{ i18n.t("features.v3welcome.list.item1") }</li>
}
<li>{ i18n.t("features.v3welcome.list.item1") }</li>
<li>{ i18n.t("features.v3welcome.list.item2") }</li>
<li>{ i18n.t("features.v3welcome.list.item3") }</li>
<li>{ i18n.t("features.v3welcome.list.item4") }</li>
@@ -39,7 +38,10 @@ export default function WelcomeDialog(): React.ReactElement
<fui.DialogActions>
<fui.DialogTrigger disableButtonEnhancement>
<fui.Button appearance="primary" as="a" { ...extLink(v3blogPost) }>
<fui.Button
appearance="primary" as="a" { ...extLink(v3blogPost) }
onClick={ () => track("visit_blog_button_click") }
>
{ i18n.t("features.v3welcome.actions.visit_blog") }
</fui.Button>
</fui.DialogTrigger>
+15 -7
View File
@@ -1,17 +1,25 @@
import { useDialog } from "@/contexts/DialogProvider";
import { DialogContextType } from "@/contexts/DialogProvider";
import WelcomeDialog from "../components/WelcomeDialog";
import { showWelcomeDialog } from "../utils/showWelcomeDialog";
export default function useWelcomeDialog(): void
export default function useWelcomeDialog(dialog: DialogContextType): Promise<void>
{
const dialog = useDialog();
useEffect(() =>
return new Promise<void>(res =>
{
showWelcomeDialog.getValue().then(showWelcome =>
{
if (showWelcome || import.meta.env.DEV)
dialog.pushCustom(<WelcomeDialog />, undefined, () => showWelcomeDialog.removeValue());
dialog.pushCustom(
<WelcomeDialog />,
undefined,
() =>
{
showWelcomeDialog.removeValue();
res();
}
);
else
res();
});
}, []);
});
}
+18
View File
@@ -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));
}
})();
}, []);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { makeStyles, tokens } from "@fluentui/react-components";
export const useGroupColors: () => Record<chrome.tabGroups.ColorEnum, string> = makeStyles({
export const useGroupColors: () => Record<`${Browser.tabGroups.Color}`, string> = makeStyles({
blue:
{
"--border": tokens.colorPaletteBlueBorderActive,
+2 -2
View File
@@ -14,8 +14,8 @@ export default function useStorageInfo(): StorageInfoHook
return {
bytesInUse,
storageQuota: chrome.storage.sync.QUOTA_BYTES ?? 102400,
usedStorageRatio: bytesInUse / (chrome.storage.sync.QUOTA_BYTES ?? 102400)
storageQuota: browser.storage.sync.QUOTA_BYTES ?? 102400,
usedStorageRatio: bytesInUse / (browser.storage.sync.QUOTA_BYTES ?? 102400)
};
}
+21 -1
View File
@@ -36,6 +36,15 @@ features:
text3: "Visit our dev blog to learn more about this update and all of its features!"
actions:
visit_blog: "Read dev blog"
settingsReview:
title: "Review your settings"
action: "All settings"
analytics:
title: "These statistics will help us improve the extension"
p1: "We only collect usage statistics (number of collections, used features, etc.)"
p2: "We do not collect any of your data!"
p3_text: "See the full list of what we collect"
p3_link: "here"
notifications:
tabs_saved:
@@ -73,7 +82,9 @@ options_page:
show_delete_prompt: "Ask for confirmation when deleting an item"
show_badge: "Show counter badge"
show_notification: "Show notification when saving tabs using context menu"
show_partial_save_notification: "Show notification when some tabs couldn't be saved"
unload_tabs: "Do not load tabs after opening"
allow_analytics: "Allow collection of anonymous statistics"
list_locations:
title: "Open collection list in:"
options:
@@ -121,6 +132,13 @@ options_page:
disable_prompt:
text: "This action will disable collection synchronization between your devices. Extension's settings will still be synchronized."
action: "Disable and reload the extension"
thumbnail_capture: "Capture thumbnails and icons for saved tabs"
thumbnail_capture_notice1: "Requires permission to access content on visited websites"
thumbnail_capture_notice2: "Disabling this feature may improve performance on large collections"
clear_thumbnails:
action: "Clear saved thumbnails"
title: "Delete all saved thumbnails?"
prompt: "This action will remove all saved thumbnails, previews and icons for your saved tabs. This action cannot be undone."
about:
title: "About"
developed_by: "Developed by Eugene Fox"
@@ -133,6 +151,7 @@ options_page:
website: "My website"
source: "Source code"
changelog: "Changelog"
privacy: "Privacy policy"
collections:
empty: "This collection is empty"
@@ -161,8 +180,8 @@ collections:
menu:
delete: "Delete collection"
add_selected: "Add selected tabs"
add_all: "Add all tabs"
add_group: "Add empty group"
add_pinned: "Add pinned group"
export_bookmarks: "Export to bookmarks"
edit: "Edit collection"
@@ -174,6 +193,7 @@ groups:
menu:
new_window: "Open in new window"
add_selected: "Add selected tabs"
add_all: "Add all tabs"
edit: "Edit group"
ungroup: "Ungroup"
delete: "Delete group"
+270
View File
@@ -0,0 +1,270 @@
manifest:
name: "Pestañas a un lado"
description: "Guarda y organiza tus pestañas para más tarde. Retoma donde lo dejaste"
author: "Eugene Fox"
shortcuts:
toggle_sidebar: "Abrir lista de colecciones"
set_aside: "Apartar pestañas"
save_tabs: "Guardar pestañas sin cerrar"
common:
actions:
cancel: "Cancelar"
save: "Guardar"
close: "Cerrar"
delete: "Eliminar"
reset_filters: "Restablecer filtros"
cta:
feedback: "Dejar comentarios"
sponsor: "Invítame un café"
tooltips:
more: "Más"
delete_prompt: "¿Estás seguro? Esta acción no se puede deshacer."
features:
v3welcome:
title: "Bienvenido a Pestañas a un lado 3.0"
text1: "¡Estamos felices de anunciar nuestra nueva actualización principal para la extensión Pestañas a un lado!"
text2: "Esta actualización trae una nueva interfaz de usuario y muchas características nuevas, incluyendo:"
list:
item1: "Soporte para grupos de pestañas"
item2: "Personalización de colecciones"
item3: "Reordenamiento y organización mediante arrastrar y soltar"
item4: "Creación manual de colecciones desde cero"
item5: "¡Y más!"
text3: "¡Visita nuestro blog de desarrollo para aprender más sobre esta actualización y todas sus características!"
actions:
visit_blog: "Leer el blog de desarrollo"
settingsReview:
title: "Revisa tus ajustes"
action: "Todos los ajustes"
analytics:
title: "Estas estadísticas nos ayudarán a mejorar la extensión"
p1: "Solo recopilamos estadísticas de uso (número de colecciones, funciones utilizadas, etc.)"
p2: "¡No recopilamos ninguno de tus datos!"
p3_text: "Ver la lista completa de lo que recopilamos"
p3_link: "aquí"
notifications:
tabs_saved:
title: "Nueva colección creada"
message: "Tus pestañas se han guardado en una nueva colección"
error_quota_exceeded:
title: "Se excedió el máximo de operaciones de escritura en la nube"
message: "Guardamos tus pestañas en el almacenamiento local. Tendrás que actualizar manualmente tu almacenamiento en la nube"
error_storage_full:
title: "Tu almacenamiento en la nube está lleno"
message: "Guardamos tus pestañas en el almacenamiento local. Por favor, libera espacio en tu almacenamiento en la nube"
bookmark_saved:
title: "Exportado a marcadores"
message: "Tu colección ha sido exportada a marcadores"
partial_save:
title: "Algunas pestañas no se pudieron guardar"
message: "Algunas de las pestañas eran pestañas del sistema a las que no pudimos acceder. Fueron omitidas"
actions:
save:
all: "Guardar todas las pestañas"
selected: "Guardar pestañas seleccionadas"
set_aside:
all: "Apartar todas las pestañas"
selected: "Apartar pestañas seleccionadas"
show_collections: "Mostrar colecciones"
options_page:
title: "Configuración"
general:
title: "General"
options:
always_show_toolbars: "Mostrar siempre las barras de herramientas"
include_pinned: "Incluir pestañas fijadas al guardar todas las pestañas"
show_delete_prompt: "Pedir confirmación al eliminar un elemento"
show_badge: "Mostrar insignia de contador"
show_notification: "Mostrar notificación al guardar pestañas usando el menú contextual"
show_partial_save_notification: "Mostrar notificación cuando algunas pestañas no se pudieron guardar"
unload_tabs: "No cargar pestañas después de abrir"
allow_analytics: "Permitir la recopilación de estadísticas anónimas"
list_locations:
title: "Abrir lista de colecciones en:"
options:
sidebar: "Barra lateral"
popup: "Ventana emergente"
tab: "Pestaña separada"
pinned: "Pestaña separada fijada"
icon_action:
title: "Al hacer clic en el icono de la extensión:"
options:
action: "Realizar la acción de guardado predeterminada"
context: "Mostrar menú contextual"
open: "Abrir lista de colecciones"
change_shortcuts: "Cambiar atajos de la extensión"
actions:
title: "Acciones predeterminadas"
options:
save_actions:
title: "Acción predeterminada al guardar pestañas"
options:
set_aside: "Guardar y cerrar pestañas"
save: "Guardar pestañas sin cerrar"
restore_actions:
title: "Acción predeterminada al abrir colecciones"
options:
open: "Solo abrir las pestañas"
restore: "Abrir pestañas y eliminar la colección"
storage:
title: "Almacenamiento"
capacity:
title: "Capacidad de almacenamiento en la nube"
description: "$1 de $2 KiB"
import: "Importar datos"
export: "Exportar datos"
import_results:
success: "Datos importados con éxito"
error: "El archivo proporcionado parece estar corrupto. No se importó nada"
import_prompt:
title: "Importar datos"
warning_title: "Esta es una acción irreversible"
warning_text: "Esto sobrescribirá todos tus datos. Asegúrate de haber elegido el archivo correcto, de lo contrario, podría ocurrir corrupción o pérdida de datos. Se recomienda exportar los datos primero."
proceed: "Seleccionar un archivo"
enable: "Habilitar almacenamiento en la nube"
disable: "Deshabilitar almacenamiento en la nube"
disable_prompt:
text: "Esta acción deshabilitará la sincronización de colecciones entre tus dispositivos. La configuración de la extensión seguirá sincronizándose."
action: "Deshabilitar y recargar la extensión"
thumbnail_capture: "Capturar miniaturas e íconos para las pestañas guardadas"
thumbnail_capture_notice1: "Requiere permiso para acceder al contenido de los sitios web visitados"
thumbnail_capture_notice2: "Deshabilitar esta función puede mejorar el rendimiento en colecciones grandes"
clear_thumbnails:
action: "Eliminar miniaturas guardadas"
title: "¿Eliminar todas las miniaturas guardadas?"
prompt: "Esta acción eliminará todas las miniaturas, vistas previas e íconos guardados para tus pestañas guardadas. Esta acción no se puede deshacer."
about:
title: "Acerca de"
developed_by: "Desarrollado por Eugene Fox"
licensed_under: "Licenciado bajo"
mit_license: "Licencia MIT"
translation_cta:
text: "¿Encontraste un error tipográfico o quieres una traducción para tu idioma?"
button: "Comienza aquí"
links:
website: "Mi sitio web"
source: "Código fuente"
changelog: "Registro de cambios"
privacy: "Política de privacidad"
collections:
empty: "Esta colección está vacía"
tabs_count: "$1 pestañas"
actions:
open: "Abrir todo"
restore: "Restaurar todo"
new_window: "Abrir todo en una ventana nueva"
incognito:
edge: "Abrir todo en ventana InPrivate"
firefox: "Abrir todo en una nueva ventana privada"
chrome: "Abrir todo en una ventana de incógnito"
incognito_check:
title: "Permisos requeridos"
message:
edge:
p1: "La extensión necesita permiso para abrir pestañas en ventana InPrivate"
p2: "Para hacerlo, haz clic en \"Configuración\" y luego selecciona la opción \"Permitir en InPrivate\""
firefox:
p1: "La extensión necesita permiso para abrir pestañas en una ventana privada"
p2: "Para hacerlo, haz clic en \"Configuración\", ve a \"Detalles\" y configura \"Ejecutar en ventana privada\" en \"Permitir\""
chrome:
p1: "La extensión necesita permiso para abrir pestañas en una ventana de incógnito"
p2: "Para hacerlo, haz clic en \"Configuración\" y luego selecciona la opción \"Permitir en incógnito\""
action: "Configuración"
menu:
delete: "Eliminar colección"
add_selected: "Agregar pestañas seleccionadas"
add_all: "Agregar todas las pestañas"
add_group: "Agregar grupo vacío"
export_bookmarks: "Exportar a marcadores"
edit: "Editar colección"
groups:
title: "Grupo"
pinned: "Fijado"
open: "Abrir todo"
empty: "Este grupo está vacío"
menu:
new_window: "Abrir en una nueva ventana"
add_selected: "Agregar pestañas seleccionadas"
add_all: "Agregar todas las pestañas"
edit: "Editar grupo"
ungroup: "Desagrupar"
delete: "Eliminar grupo"
tabs:
delete: "Eliminar pestaña"
colors:
none: "Sin color"
any: "Cualquier color"
grey: "Gris"
blue: "Azul"
red: "Rojo"
yellow: "Amarillo"
green: "Verde"
pink: "Rosa"
purple: "Morado"
cyan: "Cian"
orange: "Naranja"
dialogs:
edit:
title:
edit_collection: "Editar colección"
edit_group: "Editar grupo"
new_group: "Nuevo grupo"
new_collection: "Nueva colección"
collection_title: "Título"
color: "Color"
main:
header:
create_collection: "Crear nueva colección"
menu:
tiles_view: "Vista de mosaicos"
changelog: "¿Qué hay de nuevo?"
list:
searchbar:
title: "Buscar"
filter: "Filtrar"
sort:
title: "Ordenar"
options:
newest: "Más recientes primero"
oldest: "Más antiguos primero"
ascending: "De la A a la Z"
descending: "De la Z a la A"
custom: "Personalizado"
empty:
title: "Nada que mostrar aquí todavía"
message: "Aparta tus pestañas actuales o crea una nueva colección"
empty_search:
title: "No se encontró nada"
message: "Intenta cambiar tu consulta de búsqueda"
cta_message:
title: "¿Te gusta esta extensión?"
message: "Considera apoyar al autor con una donación o"
feedback: "dejando un comentario"
storage_full_message:
title: "Tu almacenamiento en la nube está casi lleno ($1%)"
message: "Puedes liberar espacio eliminando colecciones no utilizadas."
parse_error_message:
title: "No pudimos obtener colecciones de tu almacenamiento en la nube."
message: "Tu almacenamiento en la nube parece estar corrupto. Puedes solucionarlo reemplazándolo con tu copia local."
action: "Solucionar con copia local"
merge_conflict_message:
title: "Tus almacenamientos local y en la nube tienen cambios en conflicto."
message: "Para solucionarlo, puedes cargar tu copia local en la nube o aceptar los cambios de la nube."
accept_local: "Reemplazar con local"
accept_cloud: "Aceptar cambios de la nube"
+270
View File
@@ -0,0 +1,270 @@
manifest:
name: "Schede a parte"
description: "Salva e organizza le tue schede per dopo. Riprendi da dove avevi lasciato"
author: "Eugene Fox"
shortcuts:
toggle_sidebar: "Apri elenco delle collezioni"
set_aside: "Metti da parte le schede"
save_tabs: "Salva le schede senza chiuderle"
common:
actions:
cancel: "Annulla"
save: "Salva"
close: "Chiudi"
delete: "Elimina"
reset_filters: "Reimposta filtri"
cta:
feedback: "Lascia un feedback"
sponsor: "Offrimi un caffè"
tooltips:
more: "Altro"
delete_prompt: "Sei sicuro? Questa azione non può essere annullata."
features:
v3welcome:
title: "Benvenuto in Schede a parte 3.0"
text1: "Siamo felici di annunciare il nostro nuovo aggiornamento principale per l'estensione Schede a parte!"
text2: "Questo aggiornamento porta una nuova interfaccia utente e molte nuove funzionalità, tra cui:"
list:
item1: "Supporto per gruppi di schede"
item2: "Personalizzazione delle collezioni"
item3: "Riordino e organizzazione tramite drag and drop"
item4: "Creazione manuale di collezioni da zero"
item5: "E altro ancora!"
text3: "Visita il nostro blog per saperne di più su questo aggiornamento e tutte le sue funzionalità!"
actions:
visit_blog: "Leggi il blog degli sviluppatori"
settingsReview:
title: "Rivedi le tue impostazioni"
action: "Tutte le impostazioni"
analytics:
title: "Queste statistiche ci aiuteranno a migliorare l'estensione"
p1: "Raccogliamo solo statistiche di utilizzo (numero di collezioni, funzionalità utilizzate, ecc.)"
p2: "Non raccogliamo nessuno dei tuoi dati!"
p3_text: "Vedi l'elenco completo di ciò che raccogliamo"
p3_link: "qui"
notifications:
tabs_saved:
title: "Nuova collezione creata"
message: "Le tue schede sono state salvate in una nuova collezione"
error_quota_exceeded:
title: "Superato il limite massimo di operazioni di scrittura sul cloud"
message: "Abbiamo salvato le tue schede nella memoria locale. Dovrai aggiornare manualmente il tuo spazio cloud"
error_storage_full:
title: "Il tuo spazio cloud è pieno"
message: "Abbiamo salvato le tue schede nella memoria locale. Libera spazio nel tuo cloud storage"
bookmark_saved:
title: "Esportato nei segnalibri"
message: "La tua collezione è stata esportata nei segnalibri"
partial_save:
title: "Alcune schede non sono state salvate"
message: "Alcune schede erano schede di sistema a cui non potevamo accedere. Sono state saltate"
actions:
save:
all: "Salva tutte le schede"
selected: "Salva le schede selezionate"
set_aside:
all: "Metti da parte tutte le schede"
selected: "Metti da parte le schede selezionate"
show_collections: "Mostra collezioni"
options_page:
title: "Impostazioni"
general:
title: "Generale"
options:
always_show_toolbars: "Mostra sempre le barre degli strumenti"
include_pinned: "Includi schede bloccate quando salvi tutte le schede"
show_delete_prompt: "Chiedi conferma quando elimini un elemento"
show_badge: "Mostra il badge del contatore"
show_notification: "Mostra notifica quando salvi le schede usando il menu contestuale"
show_partial_save_notification: "Mostra notifica quando alcune schede non sono state salvate"
unload_tabs: "Non caricare le schede dopo l'apertura"
allow_analytics: "Consenti la raccolta di statistiche anonime"
list_locations:
title: "Apri elenco delle collezioni in:"
options:
sidebar: "Barra laterale"
popup: "Popup"
tab: "Scheda separata"
pinned: "Scheda separata bloccata"
icon_action:
title: "Quando clicchi sull'icona dell'estensione:"
options:
action: "Esegui l'azione di salvataggio predefinita"
context: "Mostra menu contestuale"
open: "Apri elenco delle collezioni"
change_shortcuts: "Modifica le scorciatoie dell'estensione"
actions:
title: "Azioni predefinite"
options:
save_actions:
title: "Azione predefinita quando salvi le schede"
options:
set_aside: "Salva e chiudi le schede"
save: "Salva le schede senza chiuderle"
restore_actions:
title: "Azione predefinita quando apri le collezioni"
options:
open: "Apri solo le schede"
restore: "Apri le schede e rimuovi la collezione"
storage:
title: "Archiviazione"
capacity:
title: "Capacità di archiviazione cloud"
description: "$1 di $2 KiB"
import: "Importa dati"
export: "Esporta dati"
import_results:
success: "Dati importati con successo"
error: "Il file fornito sembra essere corrotto. Non è stato importato nulla"
import_prompt:
title: "Importa dati"
warning_title: "Questa è un'azione irreversibile"
warning_text: "Questo sovrascriverà tutti i tuoi dati. Assicurati di aver scelto il file corretto, altrimenti potrebbero verificarsi corruzioni o perdite di dati. Si consiglia di esportare prima i dati."
proceed: "Scegli un file"
enable: "Abilita archiviazione cloud"
disable: "Disabilita archiviazione cloud"
disable_prompt:
text: "Questa azione disabiliterà la sincronizzazione delle collezioni tra i tuoi dispositivi. Le impostazioni dell'estensione saranno comunque sincronizzate."
action: "Disabilita e ricarica l'estensione"
thumbnail_capture: "Cattura miniature e icone per le schede salvate"
thumbnail_capture_notice1: "Richiede il permesso di accedere ai contenuti dei siti web visitati"
thumbnail_capture_notice2: "Disabilitare questa funzione può migliorare le prestazioni su collezioni di grandi dimensioni"
clear_thumbnails:
action: "Elimina miniature salvate"
title: "Eliminare tutte le miniature salvate?"
prompt: "Questa azione rimuoverà tutte le miniature, anteprime e icone salvate per le tue schede salvate. Questa azione non può essere annullata."
about:
title: "Informazioni"
developed_by: "Sviluppato da Eugene Fox"
licensed_under: "Concesso in licenza sotto"
mit_license: "Licenza MIT"
translation_cta:
text: "Hai trovato un errore di battitura o vuoi una traduzione per la tua lingua?"
button: "Inizia qui"
links:
website: "Il mio sito web"
source: "Codice sorgente"
changelog: "Registro delle modifiche"
privacy: "Politica sulla riservatezza"
collections:
empty: "Questa collezione è vuota"
tabs_count: "$1 schede"
actions:
open: "Apri tutto"
restore: "Ripristina tutto"
new_window: "Apri tutto in una nova finestra"
incognito:
edge: "Apri tutto in una nuova finestra InPrivate"
firefox: "Apri tutto in nuova finestra anonima"
chrome: "Apri tutto in finestra di navigazione in incognito"
incognito_check:
title: "Permessi richiesti"
message:
edge:
p1: "L'estensione necessita del permesso per aprire schede in finestra InPrivate"
p2: "Per farlo, clicca su \"Impostazioni\" e poi seleziona l'opzione \"Consenti in InPrivate\""
firefox:
p1: "L'estensione necessita del permesso per aprire schede in finestra anonima"
p2: "Per farlo, clicca su \"Impostazioni\", vai su \"Dettagli\" e imposta \"Funzionamento in finestre anonime\" su \"Consenti\""
chrome:
p1: "L'estensione necessita del permesso per aprire schede in finestra di navigazione in incognito"
p2: "Per farlo, clicca su \"Impostazioni\" e poi seleziona l'opzione \"Consenti modalità di navigazione in incognito\""
action: "Impostazioni"
menu:
delete: "Elimina collezione"
add_selected: "Aggiungi schede selezionate"
add_all: "Aggiungi tutte le schede"
add_group: "Aggiungi gruppo vuoto"
export_bookmarks: "Esporta nei segnalibri"
edit: "Modifica collezione"
groups:
title: "Gruppo"
pinned: "Bloccato"
open: "Apri tutto"
empty: "Questo gruppo è vuoto"
menu:
new_window: "Apri in una nuova finestra"
add_selected: "Aggiungi schede selezionate"
add_all: "Aggiungi tutte le schede"
edit: "Modifica gruppo"
ungroup: "Rimuovi dal gruppo"
delete: "Elimina gruppo"
tabs:
delete: "Elimina scheda"
colors:
none: "Nessun colore"
any: "Qualsiasi colore"
grey: "Grigio"
blue: "Blu"
red: "Rosso"
yellow: "Giallo"
green: "Verde"
pink: "Rosa"
purple: "Viola"
cyan: "Ciano"
orange: "Arancione"
dialogs:
edit:
title:
edit_collection: "Modifica collezione"
edit_group: "Modifica gruppo"
new_group: "Nuovo gruppo"
new_collection: "Nuova collezione"
collection_title: "Titolo"
color: "Colore"
main:
header:
create_collection: "Crea nuova collezione"
menu:
tiles_view: "Vista a riquadri"
changelog: "Cosa c'è di nuovo?"
list:
searchbar:
title: "Cerca"
filter: "Filtra"
sort:
title: "Ordina"
options:
newest: "Più recenti prima"
oldest: "Più vecchi prima"
ascending: "Dalla A alla Z"
descending: "Dalla Z alla A"
custom: "Personalizzato"
empty:
title: "Niente da mostrare qui per ora"
message: "Metti da parte le tue schede attuali o crea una nuova collezione"
empty_search:
title: "Non è stato trovato nulla"
message: "Prova a cambiare la tua query di ricerca"
cta_message:
title: "Ti piace questa estensione?"
message: "Considera di supportare l'autore con una donazione o"
feedback: "lasciando un feedback"
storage_full_message:
title: "Il tuo spazio cloud è quasi pieno ($1%)"
message: "Puoi liberare spazio eliminando collezioni inutilizzate."
parse_error_message:
title: "Non siamo riusciti a ottenere le collezioni dal tuo spazio cloud."
message: "Il tuo spazio cloud sembra essere corrotto. Puoi risolvere sostituendolo con la tua copia locale."
action: "Risolvere con copia locale"
merge_conflict_message:
title: "Le tue memorie locali e cloud hanno modifiche in conflitto."
message: "Per risolvere, puoi caricare la tua copia locale nel cloud o accettare le modifiche del cloud."
accept_local: "Sostituisci con locale"
accept_cloud: "Accetta modifiche del cloud"
+270
View File
@@ -0,0 +1,270 @@
manifest:
name: "Odłożone karty"
description: "Odkładaj i organizuj swoje karty. Kontynuuj tam, gdzie przerwałeś"
author: "Eugeniusz Lis"
shortcuts:
toggle_sidebar: "Otwórz listę kolekcji"
set_aside: "Odłóż karty"
save_tabs: "Zapisz karty"
common:
actions:
cancel: "Anuluj"
save: "Zapisz"
close: "Zamknij"
delete: "Usuń"
reset_filters: "Resetuj filtry"
cta:
feedback: "Zostaw opinię"
sponsor: "Wesprzyj"
tooltips:
more: "Więcej"
delete_prompt: "Czy jesteś pewien? Tej akcji nie można cofnąć."
features:
v3welcome:
title: "Witamy w Odłożonych kartach 3.0"
text1: "Z radością przedstawiamy nową dużą aktualizację rozszerzenia!"
text2: "Ta aktualizacja zawiera zupełnie nowy interfejs i wiele nowych funkcji, takich jak:"
list:
item1: "Obsługa grupowania kart"
item2: "Personalizacja kolekcji"
item3: "Przeciąganie kolekcji i elementów"
item4: "Tworzenie kolekcji od zera"
item5: "I wiele więcej!"
text3: "Odwiedź blog dewelopera (tylko w języku angielskim), aby dowiedzieć się więcej o tej aktualizacji i wszystkich jej funkcjach!"
actions:
visit_blog: "Czytaj blog"
settingsReview:
title: "Sprawdź ustawienia"
action: "Wszystkie ustawienia"
analytics:
title: "Ta statystyka pozwoli ulepszać rozszerzenie"
p1: "Zbieramy tylko statystyki użycia (liczba kolekcji, używane funkcje itp.)"
p2: "Nie zbieramy twoich danych osobowych!"
p3_text: "Pełną listę zbieranych danych można zobaczyć"
p3_link: "tutaj"
notifications:
tabs_saved:
title: "Utworzono nową kolekcję"
message: "Twoje karty zostały zapisane w nowej kolekcji"
error_quota_exceeded:
title: "Przekroczono limit operacji zapisu w chmurze"
message: "Twoje karty zostały zapisane w lokalnym magazynie. Musisz ręcznie zaktualizować magazyn w chmurze"
error_storage_full:
title: "Magazyn w chmurze jest pełny"
message: "Twoje karty zostały zapisane w lokalnym magazynie. Proszę zwolnić miejsce w magazynie w chmurze"
bookmark_saved:
title: "Wyeksportowano do zakładek"
message: "Twoja kolekcja została wyeksportowana do zakładek"
partial_save:
title: "Niektóre karty nie zostały zapisane"
message: "Niektóre z kart są systemowe i nie mogą być zapisane"
actions:
save:
all: "Zapisz wszystkie karty"
selected: "Zapisz wybrane karty"
set_aside:
all: "Odłóż wszystkie karty"
selected: "Odłóż wybrane karty"
show_collections: "Pokaż listę kolekcji"
options_page:
title: "Ustawienia"
general:
title: "Ogólne"
options:
always_show_toolbars: "Zawsze pokazuj paski narzędzi"
include_pinned: "Zapisuj przypięte karty przy zapisywaniu wszystkich kart"
show_delete_prompt: "Pytaj o potwierdzenie przy usuwaniu elementów"
show_badge: "Pokaż licznik"
show_notification: "Pokaż powiadomienie przy zapisywaniu przez menu kontekstowe"
show_partial_save_notification: "Pokaż powiadomienie, jeśli niektóre karty nie zostały zapisane"
unload_tabs: "Nie ładuj kart po otwarciu"
allow_analytics: "Zezwól na zbieranie anonimowej statystyki"
list_locations:
title: "Otwieraj listę kolekcji w:"
options:
sidebar: "Panel boczny"
popup: "Okno popup"
tab: "Osobna karta"
pinned: "Przypięta karta"
icon_action:
title: "Po kliknięciu ikony rozszerzenia:"
options:
action: "Zapisz karty (domyślna akcja)"
context: "Pokaż menu kontekstowe"
open: "Otwórz listę kolekcji"
change_shortcuts: "Zmień skróty klawiszowe"
actions:
title: "Akcje"
options:
save_actions:
title: "Domyślna akcja przy zapisywaniu kart"
options:
set_aside: "Zapisz i zamknij karty"
save: "Zapisz karty bez ich zamykania"
restore_actions:
title: "Domyślna akcja przy otwieraniu kolekcji"
options:
open: "Po prostu otwórz karty"
restore: "Otwórz karty i usuń kolekcję"
storage:
title: "Magazyn"
capacity:
title: "Magazyn w chmurze"
description: "$1 z $2 KiB"
import: "Importuj dane"
export: "Eksportuj dane"
import_results:
success: "Dane zostały pomyślnie zaimportowane"
error: "Wygląda na to, że wybrany plik jest uszkodzony. Nic nie zostało zaimportowane"
import_prompt:
title: "Import danych"
warning_title: "To jest nieodwracalna akcja!"
warning_text: "Zastąpi wszystkie twoje dane. Upewnij się, że wybrałeś właściwy plik, w przeciwnym razie może to prowadzić do uszkodzenia lub utraty danych. Zaleca się najpierw wyeksportować dane."
proceed: "Wybierz plik"
disable: "Wyłącz magazyn w chmurze"
enable: "Włącz magazyn w chmurze"
disable_prompt:
text: "Ta akcja wyłączy synchronizację kolekcji między twoimi urządzeniami. Ustawienia nadal będą przechowywane w chmurze."
action: "Wyłącz i przeładuj rozszerzenie"
thumbnail_capture: "Zapisuj podglądy i ikony dla zapisanych kart"
thumbnail_capture_notice1: "Wymagany dostęp do zawartości odwiedzanych stron internetowych"
thumbnail_capture_notice2: "Wyłączenie tej funkcji może poprawić wydajność przy dużej liczbie zapisanych kart"
clear_thumbnails:
action: "Usuń zapisane ikony"
title: "Usunąć podglądy i ikony?"
prompt: "Ta akcja usunie wszystkie podglądy i ikony twoich zapisanych kart. Tej akcji nie można cofnąć."
about:
title: "O rozszerzeniu"
developed_by: "Wywoływacz: Eugeniusz Lis"
licensed_under: ""
mit_license: "Licencja MIT"
translation_cta:
text: "Znalazłeś błąd lub chcesz tłumaczenie na swój język?"
button: "Zacznij tutaj"
links:
website: "Moja strona internetowa"
source: "Kod źródłowy"
changelog: "Lista zmian"
privacy: "Polityka prywatności"
collections:
empty: "Ta kolekcja jest pusta"
tabs_count: "Karty: $1"
actions:
open: "Otwórz wszystkie"
restore: "Przywróć wszystkie"
new_window: "Otwórz w nowym oknie"
incognito:
edge: "Otwórz w oknie InPrivate"
firefox: "Otwórz w nowym oknie w trybie prywatnym"
chrome: "Otwórz w oknie incognito"
incognito_check:
title: "Wymagane uprawnienie"
message:
edge:
p1: "Rozszerzenie wymaga dodatkowego uprawnienia, aby otworzyć karty w oknie InPrivate"
p2: "Aby to zrobić, kliknij \"Ustawienia\" i zaznacz opcję \"Zezwalaj w trybie InPrivate\""
firefox:
p1: "Rozszerzenie wymaga dodatkowego uprawnienia, aby otworzyć karty w trybie prywatnym"
p2: "Aby to zrobić, kliknij \"Ustawienia\", przejdź do \"Szczegóły\" i zezwól na \"Działanie w oknach prywatnych\""
chrome:
p1: "Rozszerzenie wymaga dodatkowego uprawnienia, aby otworzyć karty w oknie incognito"
p2: "Aby to zrobić, kliknij \"Ustawienia\" i zaznacz opcję \"Zezwalaj w trybie incognito\""
action: "Ustawienia"
menu:
delete: "Usuń kolekcję"
add_selected: "Dodaj wybrane karty"
add_all: "Dodaj wszystkie karty"
add_group: "Dodaj pustą grupę"
export_bookmarks: "Eksportuj do zakładek"
edit: "Edytuj kolekcję"
groups:
title: "Grupa"
pinned: "Przypięte"
open: "Otwórz"
empty: "Ta grupa jest pusta"
menu:
new_window: "Otwórz w nowym oknie"
add_selected: "Dodaj wybrane karty"
add_all: "Dodaj wszystkie karty"
edit: "Edytuj grupę"
ungroup: "Rozgrupuj"
delete: "Usuń grupę"
tabs:
delete: "Usuń zakładkę"
colors:
none: "Bez koloru"
any: "Dowolny kolor"
grey: "Szary"
blue: "Niebieski"
red: "Czerwony"
yellow: "Żółty"
green: "Zielony"
pink: "Różowy"
purple: "Purpurowy"
cyan: "Cyjan"
orange: "Pomarańczowy"
dialogs:
edit:
title:
edit_collection: "Edytuj kolekcję"
edit_group: "Edytuj grupę"
new_group: "Nowa grupa"
new_collection: "Nowa kolekcja"
collection_title: "Nazwij"
color: "Kolor"
main:
header:
create_collection: "Utwórz nową kolekcję"
menu:
tiles_view: "Kafelki"
changelog: "Co nowego?"
list:
searchbar:
title: "Szukaj"
filter: "Filtr"
sort:
title: "Sortowanie"
options:
newest: "Najpierw nowe"
oldest: "Najpierw stare"
ascending: "Od A do Z"
descending: "Od Z do A"
custom: "Niestandardowe"
empty:
title: "Na razie nic tu nie ma"
message: "Odłóż bieżące zakładki lub utwórz nową kolekcję"
empty_search:
title: "Nic nie znaleziono"
message: "Spróbuj zmienić zapytanie wyszukiwania"
cta_message:
title: "Podoba Ci się rozszerzenie?"
message: "Wesprzyj autora darowizną lub"
feedback: "zostaw opinię"
storage_full_message:
title: "Magazyn w chmurze prawie pełny ($1%)"
message: "Możesz zwolnić miejsce, usuwając nieużywane kolekcje."
parse_error_message:
title: "Nie udało się pobrać kolekcji z magazynu w chmurze."
message: "Wygląda na to, że magazyn w chmurze jest uszkodzony. Aby to naprawić, możesz zastąpić go lokalną kopią."
action: "Użyj lokalnej kopii"
merge_conflict_message:
title: "W lokalnym i chmurowym magazynie są konfliktujące zmiany."
message: "Aby to naprawić, możesz zapisać lokalną kopię w chmurze lub zaakceptować zmiany z chmury."
accept_local: "Zastąp lokalną"
accept_cloud: "Zaakceptuj zmiany z chmury"
+270
View File
@@ -0,0 +1,270 @@
manifest:
name: "Tabs aside"
description: "Salve e organize suas abas para depois. Continue de onde parou"
author: "Eugene Fox"
shortcuts:
toggle_sidebar: "Abrir lista de coleções"
set_aside: "Colocar abas de lado"
save_tabs: "Salvar abas sem fechar"
common:
actions:
cancel: "Cancelar"
save: "Salvar"
close: "Fechar"
delete: "Excluir"
reset_filters: "Limpar filtros"
cta:
feedback: "Deixar feedback"
sponsor: "Me pague um café"
tooltips:
more: "Mais"
delete_prompt: "Tem certeza? Esta ação não pode ser desfeita."
features:
v3welcome:
title: "Bem-vindo ao Tabs aside 3.0"
text1: "Estamos felizes em anunciar nossa nova grande atualização para a extensão Tabs aside!"
text2: "Esta atualização traz uma nova interface e muitos novos recursos, incluindo:"
list:
item1: "Suporte a grupos de abas"
item2: "Personalização de coleções"
item3: "Reordenação e organização por arrastar e soltar"
item4: "Criação manual de coleções do zero"
item5: "E mais!"
text3: "Visite nosso blog de desenvolvimento para saber mais sobre esta atualização e todos os seus recursos!"
actions:
visit_blog: "Ler blog de desenvolvimento"
settingsReview:
title: "Revise suas configurações"
action: "Todas as configurações"
analytics:
title: "Estas estatísticas nos ajudarão a melhorar a extensão"
p1: "Nós coletamos apenas estatísticas de uso (número de coleções, recursos usados, etc.)"
p2: "Nós não coletamos nenhum dos seus dados!"
p3_text: "Veja a lista completa do que coletamos"
p3_link: "aqui"
notifications:
tabs_saved:
title: "Nova coleção criada"
message: "Suas abas foram salvas em uma nova coleção"
error_quota_exceeded:
title: "Limite máximo de operações na nuvem excedido"
message: "Salvamos suas abas no armazenamento local. Você precisará atualizar o armazenamento na nuvem manualmente"
error_storage_full:
title: "Seu armazenamento na nuvem está cheio"
message: "Salvamos suas abas no armazenamento local. Por favor, libere espaço na nuvem"
bookmark_saved:
title: "Exportado para favoritos"
message: "Sua coleção foi exportada para os favoritos"
partial_save:
title: "Algumas abas não puderam ser salvas"
message: "Algumas abas eram abas do sistema que não pudemos acessar. Elas foram ignoradas"
actions:
save:
all: "Salvar todas as abas"
selected: "Salvar abas selecionadas"
set_aside:
all: "Colocar todas as abas de lado"
selected: "Colocar abas selecionadas de lado"
show_collections: "Mostrar coleções"
options_page:
title: "Configurações"
general:
title: "Geral"
options:
always_show_toolbars: "Sempre mostrar barras de ferramentas"
include_pinned: "Incluir abas fixadas ao salvar todas as abas"
show_delete_prompt: "Pedir confirmação ao excluir um item"
show_badge: "Mostrar contador no ícone"
show_notification: "Mostrar notificação ao salvar abas pelo menu de contexto"
show_partial_save_notification: "Mostrar notificação quando algumas abas não puderam ser salvas"
unload_tabs: "Não carregar abas após abrir"
allow_analytics: "Permitir coleta de estatísticas anônimas"
list_locations:
title: "Abrir lista de coleções em:"
options:
sidebar: "Barra lateral"
popup: "Popup"
tab: "Aba separada"
pinned: "Aba fixada separada"
icon_action:
title: "Ao clicar no ícone da extensão:"
options:
action: "Executar ação padrão de salvar"
context: "Mostrar menu de contexto"
open: "Abrir lista de coleções"
change_shortcuts: "Alterar atalhos da extensão"
actions:
title: "Ações padrão"
options:
save_actions:
title: "Ação padrão ao salvar abas"
options:
set_aside: "Salvar e fechar abas"
save: "Salvar abas sem fechar"
restore_actions:
title: "Ação padrão ao abrir coleções"
options:
open: "Apenas abrir abas"
restore: "Abrir abas e remover a coleção"
storage:
title: "Armazenamento"
capacity:
title: "Capacidade de armazenamento na nuvem"
description: "$1 de $2 KiB"
import: "Importar dados"
export: "Exportar dados"
import_results:
success: "Dados importados com sucesso"
error: "O arquivo fornecido parece estar corrompido. Nada foi importado"
import_prompt:
title: "Importar dados"
warning_title: "Esta é uma ação irreversível"
warning_text: "Isso irá sobrescrever todos os seus dados. Certifique-se de ter escolhido o arquivo correto, caso contrário pode ocorrer corrupção ou perda de dados. Recomenda-se exportar os dados antes."
proceed: "Escolher um arquivo"
enable: "Ativar armazenamento na nuvem"
disable: "Desativar armazenamento na nuvem"
disable_prompt:
text: "Esta ação desativará a sincronização de coleções entre seus dispositivos. As configurações da extensão ainda serão sincronizadas."
action: "Desativar e recarregar a extensão"
thumbnail_capture: "Capturar miniaturas e ícones para as abas salvas"
thumbnail_capture_notice1: "Requer permissão para acessar o conteúdo dos sites visitados"
thumbnail_capture_notice2: "Desativar esse recurso pode melhorar o desempenho em coleções grandes"
clear_thumbnails:
action: "Eliminar miniaturas guardadas"
title: "Excluir todas as miniaturas salvas?"
prompt: "Esta ação removerá todas as miniaturas, pré-visualizações e ícones salvos para suas abas salvas. Esta ação não pode ser desfeita."
about:
title: "Sobre"
developed_by: "Desenvolvido por Eugene Fox"
licensed_under: "Licenciado sob"
mit_license: "Licença MIT"
translation_cta:
text: "Encontrou um erro ou quer uma tradução para seu idioma?"
button: "Comece aqui"
links:
website: "Meu site"
source: "Código-fonte"
changelog: "Registro de alterações"
privacy: "Política de Privacidade"
collections:
empty: "Esta coleção está vazia"
tabs_count: "$1 abas"
actions:
open: "Abrir todas"
restore: "Restaurar todas"
new_window: "Abrir todas em nova janela"
incognito:
edge: "Abrir todas em nova janela InPrivate"
firefox: "Abrir todas em nova janela privativa"
chrome: "Abrir todas em janela anônima"
incognito_check:
title: "Permissões necessárias"
message:
edge:
p1: "A extensão precisa de permissão para abrir abas em janela InPrivate"
p2: "Para isso, clique em \"Configurações\" e marque a opção \"Permitir em InPrivate\""
firefox:
p1: "A extensão precisa de permissão para abrir abas em janela privativa"
p2: "Para isso, clique em \"Configurações\", vá em \"Detalhes\" e defina \"Executar em janelas privadas\" como \"Permitir\""
chrome:
p1: "A extensão precisa de permissão para abrir abas em janela anônima"
p2: "Para isso, clique em \"Configurações\" e marque a opção \"Permitir em modo anônimo\""
action: "Configurações"
menu:
delete: "Excluir coleção"
add_selected: "Adicionar abas selecionadas"
add_all: "Adicionar todas as abas"
add_group: "Adicionar grupo vazio"
export_bookmarks: "Exportar para favoritos"
edit: "Editar coleção"
groups:
title: "Grupo"
pinned: "Fixado"
open: "Abrir todas"
empty: "Este grupo está vazio"
menu:
new_window: "Abrir em nova janela"
add_selected: "Adicionar abas selecionadas"
add_all: "Adicionar todas as abas"
edit: "Editar grupo"
ungroup: "Desagrupar"
delete: "Excluir grupo"
tabs:
delete: "Excluir aba"
colors:
none: "Sem cor"
any: "Qualquer cor"
grey: "Cinza"
blue: "Azul"
red: "Vermelho"
yellow: "Amarelo"
green: "Verde"
pink: "Rosa"
purple: "Roxo"
cyan: "Ciano"
orange: "Laranja"
dialogs:
edit:
title:
edit_collection: "Editar coleção"
edit_group: "Editar grupo"
new_group: "Novo grupo"
new_collection: "Nova coleção"
collection_title: "Título"
color: "Cor"
main:
header:
create_collection: "Criar nova coleção"
menu:
tiles_view: "Visualização em blocos"
changelog: "O que há de novo?"
list:
searchbar:
title: "Pesquisar"
filter: "Filtrar"
sort:
title: "Ordenar"
options:
newest: "Mais recentes primeiro"
oldest: "Mais antigas primeiro"
ascending: "De A a Z"
descending: "De Z a A"
custom: "Personalizado"
empty:
title: "Nada para mostrar aqui ainda"
message: "Coloque suas abas atuais de lado ou crie uma nova coleção"
empty_search:
title: "Nada encontrado"
message: "Tente alterar sua busca"
cta_message:
title: "Gostou desta extensão?"
message: "Considere apoiar o autor com uma doação ou"
feedback: "deixando um feedback"
storage_full_message:
title: "Seu armazenamento na nuvem está quase cheio ($1%)"
message: "Você pode liberar espaço excluindo coleções não utilizadas."
parse_error_message:
title: "Não foi possível obter coleções do seu armazenamento na nuvem."
message: "Seu armazenamento na nuvem parece estar corrompido. Você pode corrigir isso substituindo pela sua cópia local."
action: "Corrigir com cópia local"
merge_conflict_message:
title: "Seu armazenamento local e na nuvem possuem alterações conflitantes."
message: "Para corrigir, você pode enviar sua cópia local para a nuvem ou aceitar as alterações da nuvem."
accept_local: "Substituir pela local"
accept_cloud: "Aceitar alterações da nuvem"
+26 -6
View File
@@ -36,6 +36,15 @@ features:
text3: "Посетите блог разработчика (только на английском), чтобы узнать больше об этом обновлении и всех его функциях!"
actions:
visit_blog: "Читать блог"
settingsReview:
title: "Проверьте настройки"
action: "Все настройки"
analytics:
title: "Эта статистика позволит улучшать расширение"
p1: "Мы собираем только статистику использования (количество коллекций, используемые функции и т.д.)"
p2: "Мы не собираем ваши личные данные!"
p3_text: "Полный список собираемых данных можно посмотреть"
p3_link: "здесь"
notifications:
tabs_saved:
@@ -73,7 +82,9 @@ options_page:
show_delete_prompt: "Спрашивать подтверждение при удалении элементов"
show_badge: "Показывать счетчик"
show_notification: "Показывать уведомление при сохранении через контекстное меню"
show_partial_save_notification: "Показывать уведомление, если некоторые вкладки не были сохранены"
unload_tabs: "Не загружать вкладки после открытия"
allow_analytics: "Разрешить сбор анонимной статистики"
list_locations:
title: "Открывать список коллекций в:"
options:
@@ -84,7 +95,7 @@ options_page:
icon_action:
title: "При нажатии на иконку расширения:"
options:
action: "Выполнить действие по умолчанию"
action: "Сохранить вкладки (действие по умолчанию)"
context: "Показать контекстное меню"
open: "Открыть список коллекций"
change_shortcuts: "Изменить горячие клавиши"
@@ -121,6 +132,13 @@ options_page:
disable_prompt:
text: "Это действие отключит синхронизацию коллекций между вашими устройствами. Настройки расширения продолжат храниться в облаке."
action: "Отключить и перезагрузить расширение"
thumbnail_capture: "Сохранять превью и иконки для сохранённых вкладок"
thumbnail_capture_notice1: "Необходим доступ к содержанию посещенных веб-сайтов"
thumbnail_capture_notice2: "Отключение этой функции может улучшить производительность при большом количестве сохраненных вкладок"
clear_thumbnails:
action: "Удалить сохранённые иконки"
title: "Удалить превью и иконки?"
prompt: "Это действие удалит все превью и иконки у ваших сохраненных вкладок. Это действие не может быть отменено."
about:
title: "О расширении"
developed_by: "Разработчик: Евгений Лис"
@@ -133,6 +151,7 @@ options_page:
website: "Мой веб-сайт"
source: "Исходный код"
changelog: "Список изменений"
privacy: "Политика конфиденциальности"
collections:
empty: "Эта коллекция пуста"
@@ -149,20 +168,20 @@ collections:
title: "Требуется разрешение"
message:
edge:
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме InPrivate"
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме InPrivate"
p2: "Для этого нажмите \"Настройки\" и затем отметьте опцию \"Разрешить в режиме InPrivate\""
firefox:
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в приватном окне"
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в приватном окне"
p2: "Для этого нажмите \"Настройки\", перейдите в \"Подробности\" и разрешите \"Запуск в приватных окнах\""
chrome:
p1: "Расширению необходимо дополнительное разрешение чтобы открыть вкладки в режиме инкогнито"
p1: "Расширению необходимо дополнительное разрешение, чтобы открыть вкладки в режиме инкогнито"
p2: "Для этого нажмите \"Настройки\" и отметьте опцию \"Разрешить использование в режиме инкогнито\""
action: "Настройки"
menu:
delete: "Удалить коллекцию"
add_selected: "Добавить выбранные вкладки"
add_all: "Добавить все вкладки"
add_group: "Добавить пустую группу"
add_pinned: "Добавить закрепленную группу"
export_bookmarks: "Экспортировать в закладки"
edit: "Редактировать коллекцию"
@@ -174,6 +193,7 @@ groups:
menu:
new_window: "Открыть в новом окне"
add_selected: "Добавить выбранные вкладки"
add_all: "Добавить все вкладки"
edit: "Редактировать группу"
ungroup: "Разгруппировать"
delete: "Удалить группу"
@@ -245,6 +265,6 @@ parse_error_message:
merge_conflict_message:
title: "В локальном и облачном хранилищах есть конфликтующие изменения."
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако, либо принять изменения из облака."
message: "Чтобы это исправить, вы можете сохранить локальную копию в облако либо принять изменения из облака."
accept_local: "Заменить локальной"
accept_cloud: "Принять облачные изменения"
+26 -6
View File
@@ -36,6 +36,15 @@ features:
text3: "Відвідайте блог розробника (тільки англійською), щоб дізнатися більше про це оновлення та всі його функції!"
actions:
visit_blog: "Читати блог"
settingsReview:
title: "Перевірте налаштування"
action: "Всi налаштування"
analytics:
title: "Ця статистика дозволить покращувати розширення"
p1: "Ми збираємо лише статистику використання (кількість колекцій, використовувані функції тощо)"
p2: "Ми не збираємо ваші особисті дані!"
p3_text: "Повний список зібраних даних можна подивитися"
p3_link: "тут"
notifications:
tabs_saved:
@@ -73,7 +82,9 @@ options_page:
show_delete_prompt: "Запитувати підтвердження при видаленні елементів"
show_badge: "Показувати лічильник"
show_notification: "Показувати сповіщення при збереженні через контекстне меню"
show_partial_save_notification: "Показувати сповіщення, якщо деякі вкладки не були збережені"
unload_tabs: "Не завантажувати вкладки після відкриття"
allow_analytics: "Дозволити збір анонімної статистики"
list_locations:
title: "Відкривати список колекцій у:"
options:
@@ -84,7 +95,7 @@ options_page:
icon_action:
title: "При натисканні на іконку розширення:"
options:
action: "Виконати дію за замовчуванням"
action: "Зберегти вкладки (дія за замовчуванням)"
context: "Показати контекстне меню"
open: "Відкрити список колекцій"
change_shortcuts: "Змінити гарячі клавіші"
@@ -121,6 +132,13 @@ options_page:
disable_prompt:
text: "Ця дія вимкне синхронізацію колекцій між вашими пристроями. Налаштування продовжать зберігатися у хмарі."
action: "Вимкнути та перезавантажити розширення"
thumbnail_capture: "Зберігати превью і іконки для збережених вкладок"
thumbnail_capture_notice1: "Необхідний доступ до вмісту відвіданих веб-сайтів"
thumbnail_capture_notice2: "Вимкнення цієї функції може покращити продуктивність при великій кількості збережених вкладок"
clear_thumbnails:
action: "Видалити збережені іконки"
title: "Видалити превью і іконки?"
prompt: "Ця дія видалить всі превью і іконки у ваших збережених вкладках. Цю дію не можна скасувати."
about:
title: "О розширенні"
developed_by: "Розробник: Євген Лис"
@@ -133,6 +151,7 @@ options_page:
website: "Мій веб-сайт"
source: "Вихідний код"
changelog: "Список змін"
privacy: "Політика конфіденційності"
collections:
empty: "Ця колекція пуста"
@@ -161,8 +180,8 @@ collections:
menu:
delete: "Видалити колекцію"
add_selected: "Додати вибрані вкладки"
add_all: "Додати всі вкладки"
add_group: "Додати порожню групу"
add_pinned: "Додати закріплену групу"
export_bookmarks: "Експортувати в закладки"
edit: "Редагувати колекцію"
@@ -174,6 +193,7 @@ groups:
menu:
new_window: "Відкрити у новому вікні"
add_selected: "Додати вибрані вкладки"
add_all: "Додати всі вкладки"
edit: "Редагувати групу"
ungroup: "Розгрупувати"
delete: "Видалити групу"
@@ -190,9 +210,9 @@ colors:
yellow: "Жовтий"
green: "Зелений"
pink: "Рожевий"
purple: "Фіолетовий"
cyan: "Голубий"
orange: "Помаранчевий"
purple: "Пурпуровий"
cyan: "Бірюзовий"
orange: "Оранжевий"
dialogs:
edit:
@@ -245,6 +265,6 @@ parse_error_message:
merge_conflict_message:
title: "В локальному і облачному хранилищах є конфліктуючі зміни."
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі, або прийняти зміни з хмари."
message: "Щоб це виправити, ви можете зберегти локальну копію в хмарі або прийняти зміни з хмари."
accept_local: "Заменить локальною"
accept_cloud: "Прийняти облачні зміни"
+270
View File
@@ -0,0 +1,270 @@
manifest:
name: "搁置的标签页"
description: "保存并组织您的标签以备后用。从您离开的地方继续"
author: "尤金·福克斯"
shortcuts:
toggle_sidebar: "打开收藏列表"
set_aside: "搁置标签页"
save_tabs: "保存标签页但不关闭"
common:
actions:
cancel: "取消"
save: "保存"
close: "关闭"
delete: "删除"
reset_filters: "重置筛选"
cta:
feedback: "留下反馈"
sponsor: "请我喝杯咖啡!"
tooltips:
more: "更多"
delete_prompt: "您确定吗?此操作无法撤销。"
features:
v3welcome:
title: "欢迎使用搁置的标签页 3.0"
text1: "我们很高兴宣布搁置的标签页扩展新的重大更新!"
text2: "此更新带来了全新的用户界面,以及许多新功能,包括:"
list:
item1: "支持标签组"
item2: "收藏自定义"
item3: "拖放排序和整理"
item4: "从零开始创建收藏"
item5: "以及更多!"
text3: "访问我们的开发博客以了解有关此更新及其所有功能的更多信息!"
actions:
visit_blog: "阅读开发博客"
settingsReview:
title: "检查您的设置"
action: "所有设置"
analytics:
title: "这些统计数据将帮助我们改进扩展"
p1: "我们只收集使用统计数据(收藏数量、使用的功能等)"
p2: "我们不会收集您的任何数据!"
p3_text: "请参阅我们收集内容的"
p3_link: "完整列表"
notifications:
tabs_saved:
title: "已创建新收藏"
message: "您的标签页已保存到新收藏中"
error_quota_exceeded:
title: "超出最大云储存写入操作"
message: "我们已将您的标签页保存到本地存储。您需要手动更新云存储"
error_storage_full:
title: "您的云存储已满"
message: "我们已将您的标签页保存到本地存储。请清理一些云存储空间"
bookmark_saved:
title: "已导出到书签"
message: "您的收藏已导出到书签"
partial_save:
title: "部分标签页无法保存"
message: "部分标签页是无法访问的系统标签页。它们已被跳过"
actions:
save:
all: "保存所有标签页"
selected: "保存选定的标签页"
set_aside:
all: "搁置所有标签页"
selected: "搁置选定的标签页"
show_collections: "显示收藏"
options_page:
title: "设置"
general:
title: "常规"
options:
always_show_toolbars: "始终显示工具栏"
include_pinned: "保存所有标签页时包括已固定的标签页"
show_delete_prompt: "删除项目时要求确认"
show_badge: "显示计数角标"
show_notification: "使用上下文菜单保存标签页时显示通知"
show_partial_save_notification: "如果某些标签页无法保存则显示通知"
unload_tabs: "打开后不加载标签页"
allow_analytics: "允许收集匿名统计数据"
list_locations:
title: "在以下位置打开收藏列表:"
options:
sidebar: "侧边栏"
popup: "弹出窗口"
tab: "单独的标签页"
pinned: "单独的固定标签页"
icon_action:
title: "单击扩展图标时:"
options:
action: "执行默认保存操作"
context: "显示上下文菜单"
open: "打开收藏列表"
change_shortcuts: "更改扩展快捷方式"
actions:
title: "默认操作"
options:
save_actions:
title: "保存标签页时的默认操作"
options:
set_aside: "保存并关闭标签页"
save: "保存标签页而不关闭"
restore_actions:
title: "打开收藏时的默认操作"
options:
open: "仅打开标签页"
restore: "打开标签页并删除收藏"
storage:
title: "存储"
capacity:
title: "云存储容量"
description: "$1 / $2 KiB"
import: "导入数据"
export: "导出数据"
import_results:
success: "数据已成功导入"
error: "提供的文件似乎已损坏。未导入任何内容"
import_prompt:
title: "导入数据"
warning_title: "这是不可逆的操作"
warning_text: "这将覆盖您的所有数据!请确保选择了正确的文件,否则可能会导致数据损坏或丢失。建议先导出数据。"
proceed: "选择文件"
enable: "启用云存储"
disable: "禁用云存储"
disable_prompt:
text: "此操作将禁用设备之间的收藏同步。扩展设置仍将同步。"
action: "禁用并重新加载扩展"
thumbnail_capture: "为已保存的标签页保存缩略图和图标"
thumbnail_capture_notice1: "需要访问已访问网站内容的权限"
thumbnail_capture_notice2: "有大量收藏时,禁用此功能可能会提高性能"
clear_thumbnails:
action: "删除已保存的图标"
title: "删除缩略图和图标?"
prompt: "此操作将删除您已保存标签页的所有缩略图和图标。此操作无法撤消。"
about:
title: "关于"
developed_by: "由尤金·福克斯开发"
licensed_under: "许可协议"
mit_license: "MIT 协议"
translation_cta:
text: "发现错别字或想为您的语言提供翻译?"
button: "快速入门"
links:
website: "我的网站"
source: "源代码"
changelog: "更新日志"
privacy: "隐私政策"
collections:
empty: "此收藏为空"
tabs_count: "$1 个标签页"
actions:
open: "打开所有"
restore: "恢复所有"
new_window: "在新窗口中打开所有"
incognito:
edge: "在新 InPrivate 窗口中打开所有"
firefox: "在新隐私窗口中打开所有"
chrome: "在隐身窗口中打开所有"
incognito_check:
title: "需要权限"
message:
edge:
p1: "扩展需要权限才能在 InPrivate 窗口中打开标签"
p2: "为此,请单击“设置”,然后勾选“允许在 InPrivate 中”选项"
firefox:
p1: "扩展需要权限才能在隐私窗口中打开标签页"
p2: "为此,请单击“设置”,转到“详细信息”并将“在隐私窗口中运行”设置为“允许”"
chrome:
p1: "扩展需要权限才能在隐身窗口中打开标签页"
p2: "为此,请单击“设置”,然后勾选“允许在隐身中”选项"
action: "设置"
menu:
delete: "删除收藏"
add_selected: "添加选定的标签页"
add_all: "添加所有标签页"
add_group: "添加空分组"
export_bookmarks: "导出到书签"
edit: "编辑收藏"
groups:
title: "分组"
pinned: "已固定"
open: "打开所有"
empty: "此分组为空"
menu:
new_window: "在新窗口中打开"
add_selected: "添加选定的标签页"
add_all: "添加所有标签页"
edit: "编辑分组"
ungroup: "取消分组"
delete: "删除分组"
tabs:
delete: "删除标签页"
colors:
none: "无颜色"
any: "任何颜色"
grey: "灰色"
blue: "蓝色"
red: "红色"
yellow: "黄色"
green: "绿色"
pink: "粉色"
purple: "紫色"
cyan: "青色"
orange: "橙色"
dialogs:
edit:
title:
edit_collection: "编辑收藏"
edit_group: "编辑分组"
new_group: "新分组"
new_collection: "新收藏"
collection_title: "标题"
color: "颜色"
main:
header:
create_collection: "创建新收藏"
menu:
tiles_view: "平铺视图"
changelog: "更新内容"
list:
searchbar:
title: "搜索"
filter: "筛选"
sort:
title: "排序"
options:
newest: "最新优先"
oldest: "最旧优先"
ascending: "从 A 到 Z"
descending: "从 Z 到 A"
custom: "自定义"
empty:
title: "这里还没有内容"
message: "搁置当前标签页,或创建新收藏"
empty_search:
title: "未找到任何内容"
message: "尝试更改搜索查询"
cta_message:
title: "喜欢这个扩展吗?"
message: "考虑支持作者捐赠,或"
feedback: "留下反馈"
storage_full_message:
title: "您的云存储几乎已满($1%"
message: "您可以通过删除未使用的收藏来释放一些空间。"
parse_error_message:
title: "我们无法从您的云存储中获取收藏。"
message: "您的云存储似乎已损坏。您可以通过用本地副本替换它来修复它。"
action: "用本地副本修复"
merge_conflict_message:
title: "您的本地和云存储有冲突的更改。"
message: "要解决此问题,您可以将本地副本上传到云端,或接受云端更改。"
accept_local: "采用本地替换云端"
accept_cloud: "接受云端更改"
+2 -2
View File
@@ -17,7 +17,7 @@ export type DefaultGroupItem =
type: "group";
pinned?: false;
title?: string;
color: chrome.tabGroups.ColorEnum;
color: `${Browser.tabGroups.Color}`;
items: TabItem[];
};
@@ -28,7 +28,7 @@ export type CollectionItem =
type: "collection";
timestamp: number;
title?: string;
color?: chrome.tabGroups.ColorEnum;
color?: `${Browser.tabGroups.Color}`;
items: (TabItem | GroupItem)[];
};
+26 -26
View File
@@ -1,46 +1,46 @@
{
"name": "tabs-aside",
"private": true,
"version": "3.0.0-rc1",
"version": "3.2.1",
"type": "module",
"scripts": {
"dev": "wxt",
"build": "wxt build --mv3",
"zip": "wxt zip --mv3",
"build": "yarn lint && wxt build --mv3",
"zip": "yarn lint && wxt zip --mv3",
"lint": "tsc --noEmit && eslint . -c eslint.config.js",
"prebuild": "yarn lint",
"prezip": "yarn lint",
"postinstall": "wxt prepare"
"prepare": "wxt prepare",
"postinstall": "yarn prepare"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fluentui/react-components": "^9.63.0",
"@fluentui/react-icons": "^2.0.298",
"@webext-core/messaging": "^2.2.0",
"@wxt-dev/i18n": "^0.2.3",
"@fluentui/react-components": "^9.72.6",
"@fluentui/react-icons": "^2.0.313",
"@webext-core/messaging": "^2.3.0",
"@wxt-dev/analytics": "^0.5.1",
"@wxt-dev/i18n": "^0.2.4",
"lzutf8": "^0.6.3",
"react": "^18.3.1",
"react-dom": "18.3.1"
"react": "~19.2.0",
"react-dom": "~19.2.0"
},
"devDependencies": {
"@eslint/css": "^0.7.0",
"@eslint/js": "^9.26.0",
"@eslint/json": "^0.12.0",
"@stylistic/eslint-plugin": "^4.2.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@wxt-dev/module-react": "^1.1.3",
"eslint": "^9.26.0",
"@eslint/css": "^0.14.1",
"@eslint/js": "^9.39.1",
"@eslint/json": "^0.14.0",
"@stylistic/eslint-plugin": "^5.5.0",
"@types/react": "~19.2.2",
"@types/react-dom": "~19.2.2",
"@wxt-dev/module-react": "^1.1.5",
"eslint": "^9.39.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.0.0",
"globals": "^16.5.0",
"scheduler": "0.23.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.1",
"vite": "^6.3.4",
"wxt": "~0.19.29"
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.2",
"wxt": "^0.20.11"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
"packageManager": "yarn@4.9.2"
}
+11 -13
View File
@@ -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.
<b>Features</b>
<ul>
<li><b>Save tabs</b>: Save all your open tabs in a single click, and restore them later</li>
<li><b>Organize tabs</b>: Create collections and subgroups to organize your saved tabs</li>
<li><b>Search tabs</b>: Quickly find the tabs you need using the search feature</li>
<li><b>Sync across devices</b>: Access your saved tabs from any device with your account</li>
<li><b>Go dark</b>: Dark mode support for a more comfortable browsing experience</li>
<li><b>Personalize</b>: Change the appearance and behavior of the extension to suit your needs</li>
</ul>
**Features**
- **Save tabs:** Save all your open tabs in a single click, and restore them later
- **Organize tabs:** Create collections and subgroups to organize your saved tabs
- **Search tabs:** Quickly find the tabs you need using the search feature
- **Sync across devices:** Access your saved tabs from any device with your account
- **Go dark:** Dark mode support for a more comfortable browsing experience
- **Personalize:** Change the appearance and behavior of the extension to suit your needs
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>
If you know how to improve this extension you can check <a href="https://github.com/xfox111/TabsAsideExtension">its GitHub Repository</a>
**Hey, it's an open-source software!**
If you know how to improve this extension you can check [its GitHub Repository](https://github.com/xfox111/TabsAsideExtension)
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)
+46
View File
@@ -0,0 +1,46 @@
<!-- Versión en texto plano (Chrome/Edge) -->
Basada en la funcionalidad original del navegador Microsoft Edge, esta extensión ha crecido mucho más allá de ser solo un almacenamiento temporal para pestañas.
Te permite guardar y gestionar tus pestañas de manera conveniente, proporcionando una gama de características que facilitan organizar y acceder a tus pestañas guardadas.
## Características
- Guardar pestañas: Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde
- Organizar pestañas: Crea colecciones y subgrupos para organizar tus pestañas guardadas
- Buscar pestañas: Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda
- Sincronizar entre dispositivos: Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta
- Modo oscuro: Soporte para modo oscuro para una experiencia de navegación más cómoda
- Personalizar: Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades
Consulta nuestra publicación en el blog sobre todas las nuevas características y mejoras en Pestañas a un lado 3.0 en:
https://at.xfox111.net/tabs-aside-3-0
## ¡Oye, es un software de código abierto!
Si sabes cómo mejorar esta extensión, puedes revisar su repositorio de GitHub en:
https://github.com/xfox111/TabsAsideExtension
Consulta el registro de cambios en:
https://github.com/xfox111/TabsAsideExtension/releases/latest
<!-- Versión en texto enriquecido (Firefox) -->
Basada en la funcionalidad original del navegador Microsoft Edge, esta extensión ha crecido mucho más allá de ser solo un almacenamiento temporal para pestañas.
Te permite guardar y gestionar tus pestañas de manera conveniente, proporcionando una gama de características que facilitan organizar y acceder a tus pestañas guardadas.
**Características**
- **Guardar pestañas:** Guarda todas tus pestañas abiertas con un solo clic y restáuralas más tarde
- **Organizar pestañas:** Crea colecciones y subgrupos para organizar tus pestañas guardadas
- **Buscar pestañas:** Encuentra rápidamente las pestañas que necesitas usando la función de búsqueda
- **Sincronizar entre dispositivos:** Accede a tus pestañas guardadas desde cualquier dispositivo con tu cuenta
- **Modo oscuro:** Soporte para modo oscuro para una experiencia de navegación más cómoda
- **Personalizar:** Cambia la apariencia y el comportamiento de la extensión para adaptarla a tus necesidades
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
**¡Oye, es un software de código abierto!**
Si sabes cómo mejorar esta extensión, puedes revisar [su repositorio de GitHub](https://github.com/xfox111/TabsAsideExtension)
Consulta el [registro de cambios](https://github.com/xfox111/TabsAsideExtension/releases/latest)
+46
View File
@@ -0,0 +1,46 @@
<!-- Versione in testo semplice (Chrome/Edge) -->
Radicata nella funzionalità originale del browser Microsoft Edge, questa estensione è cresciuta molto più di un semplice spazio di archiviazione temporaneo per le schede.
Ti consente di salvare e gestire le tue schede in modo conveniente, fornendo una gamma di funzionalità che rendono facile organizzare e accedere alle tue schede salvate.
## Funzionalità
- Salva schede: Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito
- Organizza schede: Crea collezioni e sottogruppi per organizzare le tue schede salvate
- Cerca schede: Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca
- Sincronizza tra dispositivi: Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account
- Modalità scura: Supporto per la modalità scura per un'esperienza di navigazione più confortevole
- Personalizza: Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze
Dai un'occhiata al nostro post sul blog riguardante tutte le nuove funzionalità e miglioramenti in Schede a parte 3.0 su:
https://at.xfox111.net/tabs-aside-3-0
## Ehi, è un software open-source!
Se sai come migliorare questa estensione, puoi controllare il suo repository GitHub su:
https://github.com/xfox111/TabsAsideExtension
Consulta il registro delle modifiche alla versione su:
https://github.com/xfox111/TabsAsideExtension/releases/latest
<!-- Versione in testo ricco (Firefox) -->
Radicata nella funzionalità originale del browser Microsoft Edge, questa estensione è cresciuta molto più di un semplice spazio di archiviazione temporaneo per le schede.
Ti consente di salvare e gestire le tue schede in modo conveniente, fornendo una gamma di funzionalità che rendono facile organizzare e accedere alle tue schede salvate.
**Funzionalità**
- **Salva schede:** Salva tutte le tue schede aperte con un solo clic e ripristinale in seguito
- **Organizza schede:** Crea collezioni e sottogruppi per organizzare le tue schede salvate
- **Cerca schede:** Trova rapidamente le schede di cui hai bisogno utilizzando la funzione di ricerca
- **Sincronizza tra dispositivi:** Accedi alle tue schede salvate da qualsiasi dispositivo con il tuo account
- **Modalità scura:** Supporto per la modalità scura per un'esperienza di navigazione più confortevole
- **Personalizza:** Cambia l'aspetto e il comportamento dell'estensione per soddisfare le tue esigenze
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
**Ehi, è un software open-source!**
Se sai come migliorare questa estensione, puoi controllare [il suo repository GitHub](https://github.com/xfox111/TabsAsideExtension)
Consulta il [registro delle modifiche](https://github.com/xfox111/TabsAsideExtension/releases/latest)
+46
View File
@@ -0,0 +1,46 @@
<!-- Plaintext version (Chrome/Edge) -->
Zainspirowane funkcją z pierwszych wersji Microsoft Edge, to rozszerzenie stało się czymś więcej niż tylko tymczasowym magazynem kart.
Pozwala wygodnie zapisywać i zarządzać kartami, oferując wiele funkcji, które ułatwiają organizację i dostęp do zapisanych kart.
## Funkcje
- Zapisywanie kart: Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później
- Organizacja kart: Twórz kolekcje i podgrupy, aby organizować zapisane karty
- Wyszukiwanie kart: Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania
- Synchronizacja między urządzeniami: Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta
- Tryb ciemny: Obsługa trybu ciemnego dla bardziej komfortowego użytkowania
- Personalizacja: Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb
Odwiedź naszego bloga, aby dowiedzieć się więcej o wszystkich nowych funkcjach i ulepszeniach w Odłożonych kartach 3.0 pod adresem:
https://at.xfox111.net/tabs-aside-3-0
## Przy okazji, to rozszerzenie open-source!
Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić jego repozytorium na GitHubie:
https://github.com/xfox111/TabsAsideExtension
Lista zmian w najnowszej wersji:
https://github.com/xfox111/TabsAsideExtension/releases/latest
<!-- Rich text version (Firefox) -->
Zainspirowane funkcją z pierwszych wersji Microsoft Edge, to rozszerzenie stało się czymś więcej niż tylko tymczasowym magazynem kart.
Pozwala wygodnie zapisywać i zarządzać kartami, oferując wiele funkcji, które ułatwiają organizację i dostęp do zapisanych kart.
**Funkcje**
- **Zapisywanie kart:** Zapisz wszystkie otwarte karty jednym kliknięciem i przywróć je później
- **Organizacja kart:** Twórz kolekcje i podgrupy, aby organizować zapisane karty
- **Wyszukiwanie kart:** Szybko znajdź potrzebne karty za pomocą funkcji wyszukiwania
- **Synchronizacja między urządzeniami:** Dostęp do zapisanych kart z dowolnego urządzenia za pomocą swojego konta
- **Tryb ciemny:** Obsługa trybu ciemnego dla bardziej komfortowego użytkowania
- **Personalizacja:** Dostosuj wygląd i działanie rozszerzenia do swoich potrzeb
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
**Przy okazji, to rozszerzenie open-source!**
Jeśli wiesz, jak ulepszyć to rozszerzenie, możesz odwiedzić [jego repozytorium na GitHubie](https://github.com/xfox111/TabsAsideExtension)
[Lista zmian w najnowszej wersji](https://github.com/xfox111/TabsAsideExtension/releases/latest)
+47
View File
@@ -0,0 +1,47 @@
<!-- Versão em texto simples (Chrome/Edge) -->
Originando-se do recurso original do navegador Microsoft Edge, esta extensão cresceu muito além de apenas um armazenamento temporário para abas.
Ela permite que você salve e gerencie suas abas de forma conveniente, oferecendo uma variedade de recursos que facilitam a organização e o acesso às abas salvas.
## Recursos
- Salvar abas: Salve todas as suas abas abertas com um único clique e restaure-as depois
- Organizar abas: Crie coleções e subgrupos para organizar suas abas salvas
- Pesquisar abas: Encontre rapidamente as abas que você precisa usando o recurso de pesquisa
- Sincronizar entre dispositivos: Acesse suas abas salvas de qualquer dispositivo com sua conta
- Modo escuro: Suporte ao modo escuro para uma experiência de navegação mais confortável
- Personalizar: Altere a aparência e o comportamento da extensão conforme suas necessidades
Confira nossa postagem no blog sobre todos os novos recursos e melhorias do Tabs Aside 3.0 em:
https://at.xfox111.net/tabs-aside-3-0
## Ei, é um software de código aberto!
Se você sabe como melhorar esta extensão, confira seu repositório no GitHub em:
https://github.com/xfox111/TabsAsideExtension
Veja o changelog das versões em:
https://github.com/xfox111/TabsAsideExtension/releases/latest
<!-- Versão rich text (Firefox) -->
Originando-se do recurso original do navegador Microsoft Edge, esta extensão cresceu muito além de apenas um armazenamento temporário para abas.
Ela permite que você salve e gerencie suas abas de forma conveniente, oferecendo uma variedade de recursos que facilitam a organização e o acesso às abas salvas.
**Recursos**
- **Salvar abas:** Salve todas as suas abas abertas com um único clique e restaure-as depois
- **Organizar abas:** Crie coleções e subgrupos para organizar suas abas salvas
- **Pesquisar abas:** Encontre rapidamente as abas que você precisa usando o recurso de pesquisa
- **Sincronizar entre dispositivos:** Acesse suas abas salvas de qualquer dispositivo com sua conta
- **Modo escuro:** Suporte ao modo escuro para uma experiência de navegação mais confortável
- **Personalizar:** Altere a aparência e o comportamento da extensão conforme suas necessidades
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
**Ei, é um software de código aberto!**
Se você sabe como melhorar esta extensão, confira [seu repositório no GitHub](https://github.com/xfox111/TabsAsideExtension)
Veja o [changelog das versões](https://github.com/xfox111/TabsAsideExtension/releases/latest)
+11 -13
View File
@@ -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
View File
@@ -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)
+46
View File
@@ -0,0 +1,46 @@
<!-- Plaintext version (Chrome/Edge) -->
源自原始 Microsoft Edge 浏览器功能,此扩展已发展为远不止是标签的临时存储。
它允许您以方便的方式保存和管理标签,提供一系列功能,使您可以轻松组织和访问已保存的标签。
## 功能
- 保存标签:一键保存所有打开的标签,并稍后恢复
- 组织标签:创建收藏和子组以组织已保存的标签
- 搜索标签:使用搜索功能快速找到所需的标签
- 跨设备同步:使用您的帐户从任何设备访问已保存的标签
- 深色模式:支持深色模式,提供更舒适的浏览体验
- 个性化:更改扩展的外观和行为以满足您的需求
查看我们关于搁置的标签页 3.0 的所有新功能和改进的博客文章:
https://at.xfox111.net/tabs-aside-3-0
## 嘿,这是一个开源软件!
如果您知道如何改进此扩展,可以查看其 GitHub 仓库:
https://github.com/xfox111/TabsAsideExtension
查看发布更新日志:
https://github.com/xfox111/TabsAsideExtension/releases/latest
<!-- Rich text version (Firefox) -->
源自原始 Microsoft Edge 浏览器功能,此扩展已发展为远不止是标签的临时存储。
它允许您以方便的方式保存和管理标签,提供一系列功能,使您可以轻松组织和访问已保存的标签。
**功能**
- **保存标签:** 一键保存所有打开的标签,并稍后恢复
- **组织标签:** 创建收藏和子组以组织已保存的标签
- **搜索标签:** 使用搜索功能快速找到所需的标签
- **跨设备同步:** 使用您的帐户从任何设备访问已保存的标签
- **深色模式:** 支持深色模式,提供更舒适的浏览体验
- **个性化:** 更改扩展的外观和行为以满足您的需求
查看我们关于 搁置的标签页 3.0 的所有新功能和改进的[博客文章](https://at.xfox111.net/tabs-aside-3-0)
**嘿,这是一个开源软件!**
如果您知道如何改进此扩展,可以查看[其 GitHub 仓库](https://github.com/xfox111/TabsAsideExtension)
查看[发布更新日志](https://github.com/xfox111/TabsAsideExtension/releases/latest)
+5 -5
View File
@@ -1,9 +1,9 @@
{
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx",
"strict": true,
"strictNullChecks": true
}
}
}
+11
View File
@@ -0,0 +1,11 @@
export async function closeTabsAsync(tabs: Browser.tabs.Tab[]): Promise<void>
{
if (tabs.length < 1)
return;
await browser.tabs.create({
active: true,
windowId: tabs[0].windowId
});
await browser.tabs.remove(tabs.map(i => i.id!));
}
+83
View File
@@ -0,0 +1,83 @@
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
export async function createCollectionFromTabs(tabs: Browser.tabs.Tab[]): Promise<CollectionItem>
{
const collection: CollectionItem = {
type: "collection",
timestamp: Date.now(),
items: []
};
if (tabs.length < 1)
return collection;
let tabIndex: number = 0;
if (tabs[tabIndex].pinned)
{
collection.items.push({ type: "group", pinned: true, items: [] });
for (; tabIndex < tabs.length; tabIndex++)
{
if (!tabs[tabIndex].pinned)
break;
(collection.items[0] as GroupItem).items.push({
type: "tab",
url: tabs[tabIndex].url!,
title: tabs[tabIndex].title
});
}
}
// Special case, if all tabs are in the same group, create a collection with the group title
if (tabs[0].groupId && tabs[0].groupId !== -1 &&
tabs.every(i => i.groupId === tabs[0].groupId)
)
{
const group = await browser.tabGroups.get(tabs[0].groupId);
collection.title = group.title;
collection.color = group.color;
tabs.forEach(i =>
collection.items.push({ type: "tab", url: i.url!, title: i.title })
);
return collection;
}
let activeGroup: number | null = null;
for (; tabIndex < tabs.length; tabIndex++)
{
const tab = tabs[tabIndex];
if (!tab.groupId || tab.groupId === -1)
{
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
activeGroup = null;
continue;
}
if (!activeGroup || activeGroup !== tab.groupId)
{
activeGroup = tab.groupId;
const group = await browser.tabGroups.get(activeGroup!);
collection.items.push({
type: "group",
color: group.color,
title: group.title,
items: []
});
}
(collection.items[collection.items.length - 1] as GroupItem).items.push({
type: "tab",
url: tab.url!,
title: tab.title
});
}
return collection;
}
+30
View File
@@ -0,0 +1,30 @@
import { settings } from "./settings";
export async function getTabsToSaveAsync(): Promise<[Browser.tabs.Tab[], number]>
{
let tabs: Browser.tabs.Tab[] = await browser.tabs.query({
currentWindow: true,
highlighted: true
});
if (tabs.length < 2)
{
const ignorePinned: boolean = await settings.ignorePinned.getValue();
tabs = await browser.tabs.query({
currentWindow: true,
pinned: ignorePinned ? false : undefined
});
}
const tabsCount: number = tabs.length;
const extension_prefix: string = browser.runtime.getURL("/");
tabs = tabs.filter(i =>
i.url
&& new URL(i.url).protocol !== "about:"
&& new URL(i.url).hostname !== "newtab"
&& !i.url!.startsWith(extension_prefix)
);
return [tabs, tabsCount - tabs.length];
}
+33 -17
View File
@@ -1,26 +1,42 @@
import { GraphicsStorage } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessenger } from "@webext-core/messaging";
import { trackError } from "@/features/analytics";
import { CollectionItem, GraphicsStorage, GroupItem } from "@/models/CollectionModels";
import { defineExtensionMessaging, ExtensionMessagingConfig, ExtensionMessenger, ExtensionSendMessageArgs, GetDataType, GetReturnType } from "@webext-core/messaging";
type ProtocolMap =
{
addThumbnail(data: { url: string; thumbnail: string; }): void;
getGraphicsCache(): GraphicsStorage;
refreshCollections(): void;
openCollection(data: { collection: CollectionItem; targetWindow: "new" | "incognito"; }): void;
openGroup(data: { group: GroupItem; newWindow: boolean; }): void;
};
const protocol: ExtensionMessenger<ProtocolMap> = defineExtensionMessaging<ProtocolMap>();
export const onMessage = protocol.onMessage;
export const sendMessage: ExtensionMessenger<ProtocolMap>["sendMessage"] = async (...args) =>
function defineMessaging(config?: ExtensionMessagingConfig): ExtensionMessenger<ProtocolMap>
{
try
{
return await protocol.sendMessage(...args);
}
catch (ex)
{
console.error(ex);
return undefined!;
}
};
const { onMessage, sendMessage, removeAllListeners }: ExtensionMessenger<ProtocolMap> = defineExtensionMessaging<ProtocolMap>(config);
return {
onMessage,
removeAllListeners,
async sendMessage<TType extends keyof ProtocolMap>(
type: TType,
data: GetDataType<ProtocolMap[TType]>,
...args: ExtensionSendMessageArgs
): Promise<GetReturnType<ProtocolMap[TType]>>
{
try
{
return await sendMessage(type, data, ...args);
}
catch (ex)
{
console.error(ex);
trackError("messaging_error", ex as Error);
return undefined!;
}
}
};
}
export const { onMessage, sendMessage } = defineMessaging({ logger: console });
-134
View File
@@ -1,134 +0,0 @@
import { CollectionItem, GroupItem } from "@/models/CollectionModels";
import { Tabs } from "wxt/browser";
import { settings } from "./settings";
import sendNotification from "./sendNotification";
export default async function saveTabsToCollection(closeTabs: boolean): Promise<CollectionItem>
{
let tabs: Tabs.Tab[] = await browser.tabs.query({
currentWindow: true,
highlighted: true
});
if (tabs.length < 2)
{
const ignorePinned: boolean = await settings.ignorePinned.getValue();
tabs = await browser.tabs.query({
currentWindow: true,
pinned: ignorePinned ? false : undefined
});
}
const [collection, tabsToClose] = await createCollectionFromTabs(tabs);
if (closeTabs)
await browser.tabs.remove(tabsToClose.map(i => i.id!));
return collection;
}
async function createCollectionFromTabs(tabs: Tabs.Tab[]): Promise<[CollectionItem, Tabs.Tab[]]>
{
if (tabs.length < 1)
return [{ type: "collection", timestamp: Date.now(), items: [] }, []];
const tabCount: number = tabs.length;
tabs = tabs.filter(i =>
i.url
&& !i.url.startsWith(browser.runtime.getURL("/"))
&& new URL(i.url).protocol !== "about:"
&& new URL(i.url).hostname !== "newtab"
);
const collection: CollectionItem = {
type: "collection",
timestamp: Date.now(),
items: []
};
let tabIndex: number = 0;
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"
});
if (tabs[tabIndex].pinned)
{
collection.items.push({ type: "group", pinned: true, items: [] });
for (; tabIndex < tabs.length; tabIndex++)
{
if (!tabs[tabIndex].pinned)
break;
(collection.items[0] as GroupItem).items.push({
type: "tab",
url: tabs[tabIndex].url!,
title: tabs[tabIndex].title
});
}
}
if (import.meta.env.FIREFOX)
{
for (; tabIndex < tabs.length; tabIndex++)
collection.items.push({ type: "tab", url: tabs[tabIndex].url!, title: tabs[tabIndex].title });
return [collection, tabs];
}
// Special case, if all tabs are in the same group, create a collection with the group title
if (tabs[0].groupId && tabs[0].groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE &&
tabs.every(i => i.groupId === tabs[0].groupId)
)
{
const group = await browser.tabGroups!.get(tabs[0].groupId);
collection.title = group.title;
collection.color = group.color;
tabs.forEach(i =>
collection.items.push({ type: "tab", url: i.url!, title: i.title })
);
return [collection, tabs];
}
let activeGroup: number | null = null;
for (; tabIndex < tabs.length; tabIndex++)
{
const tab = tabs[tabIndex];
if (!tab.groupId || tab.groupId === chrome.tabGroups.TAB_GROUP_ID_NONE)
{
collection.items.push({ type: "tab", url: tab.url!, title: tab.title });
activeGroup = null;
continue;
}
if (!activeGroup || activeGroup !== tab.groupId)
{
activeGroup = tab.groupId;
const group = await browser.tabGroups!.get(activeGroup);
collection.items.push({
type: "group",
color: group.color,
title: group.title,
items: []
});
}
(collection.items[collection.items.length - 1] as GroupItem).items.push({
type: "tab",
url: tab.url!,
title: tab.title
});
}
return [collection, tabs];
}

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