├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── auto_assign.yml ├── dependabot.yml ├── stale.yml └── workflows │ ├── auto-assign.yml │ ├── build-zip.yml │ └── greetings.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── README_ZH.md ├── chrome-extension ├── lib │ └── background │ │ ├── badge.ts │ │ ├── index.ts │ │ ├── listen.ts │ │ └── subscribe.ts ├── manifest.js ├── package.json ├── public │ ├── _locales │ │ └── en │ │ │ └── messages.json │ ├── content.css │ ├── icon-128.png │ └── icon-34.png ├── tsconfig.json ├── utils │ └── plugins │ │ └── make-manifest-plugin.ts └── vite.config.ts ├── googlef759ff453695209f.html ├── how-to-use.md ├── package.json ├── packages ├── dev-utils │ ├── index.ts │ ├── lib │ │ ├── logger.ts │ │ └── manifest-parser │ │ │ ├── impl.ts │ │ │ ├── index.ts │ │ │ └── type.ts │ ├── package.json │ └── tsconfig.json ├── hmr │ ├── index.ts │ ├── lib │ │ ├── constant.ts │ │ ├── debounce.ts │ │ ├── initClient.ts │ │ ├── initReloadServer.ts │ │ ├── injections │ │ │ ├── refresh.ts │ │ │ └── reload.ts │ │ ├── interpreter │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── plugins │ │ │ ├── index.ts │ │ │ ├── make-entry-point-plugin.ts │ │ │ └── watch-rebuild-plugin.ts │ ├── package.json │ ├── rollup.config.mjs │ ├── tsconfig.build.json │ └── tsconfig.json ├── protobuf │ ├── README.md │ ├── index.ts │ ├── lib │ │ └── protobuf │ │ │ ├── code.ts │ │ │ ├── index.ts │ │ │ └── proto │ │ │ ├── cookie.d.ts │ │ │ └── cookie.js │ ├── package.json │ ├── proto │ │ └── cookie.proto │ ├── tsconfig.json │ ├── tsup.config.ts │ └── utils │ │ ├── base64.ts │ │ ├── compress.ts │ │ └── index.ts ├── shared │ ├── README.md │ ├── index.ts │ ├── lib │ │ ├── Providers │ │ │ ├── ThemeProvider.tsx │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useTheme.ts │ │ │ └── index.tsx │ │ ├── cloudflare │ │ │ ├── api.ts │ │ │ ├── enum.ts │ │ │ └── index.ts │ │ ├── cookie │ │ │ ├── index.ts │ │ │ ├── withCloudflare.ts │ │ │ └── withStorage.ts │ │ ├── hoc │ │ │ ├── index.ts │ │ │ ├── withErrorBoundary.tsx │ │ │ └── withSuspense.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useCookieAction.ts │ │ │ ├── useStorage.ts │ │ │ └── useStorageSuspense.tsx │ │ ├── message │ │ │ └── index.ts │ │ └── utils │ │ │ └── index.ts │ ├── package.json │ ├── tsconfig.json │ └── tsup.config.ts ├── storage │ ├── index.ts │ ├── lib │ │ ├── base.ts │ │ ├── cloudflareStorage.ts │ │ ├── cookieStorage.ts │ │ ├── domainConfigStorage.ts │ │ ├── domainStatusStorage.ts │ │ ├── index.ts │ │ ├── settingsStorage.ts │ │ └── themeStorage.ts │ ├── package.json │ └── tsconfig.json ├── tailwind-config │ ├── package.json │ └── tailwind.config.js ├── tsconfig │ ├── app.json │ ├── base.json │ ├── package.json │ └── utils.json ├── ui │ ├── components.json │ ├── globals.css │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── components │ │ │ ├── DateTable │ │ │ │ └── index.tsx │ │ │ ├── Image │ │ │ │ └── index.tsx │ │ │ ├── Spinner │ │ │ │ └── index.tsx │ │ │ ├── ThemeDropdown │ │ │ │ └── index.tsx │ │ │ ├── Tooltip │ │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ └── ui │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── select.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ └── tooltip.tsx │ │ ├── index.ts │ │ └── libs │ │ │ ├── index.ts │ │ │ └── utils.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsup.config.ts └── zipper │ ├── index.mts │ ├── lib │ └── index.ts │ ├── package.json │ └── tsconfig.json ├── pages ├── options │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── github.svg │ │ └── logo.png │ ├── src │ │ ├── Options.tsx │ │ ├── components │ │ │ ├── SettingsPopover.tsx │ │ │ └── StorageSelect.tsx │ │ ├── index.css │ │ └── index.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── popup │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── github.svg │ │ └── logo.png │ ├── src │ │ ├── Popup.tsx │ │ ├── components │ │ │ └── AutoSwtich │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ └── useDomainConfig.ts │ │ ├── index.css │ │ ├── index.tsx │ │ └── utils │ │ │ └── index.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts └── sidepanel │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── logo.svg │ ├── src │ ├── SidePanel.tsx │ ├── components │ │ └── CookieTable │ │ │ ├── SearchInput.tsx │ │ │ ├── hooks │ │ │ ├── useAction.ts │ │ │ ├── useCookieItem.ts │ │ │ └── useSelected.tsx │ │ │ └── index.tsx │ ├── index.css │ ├── index.html │ └── index.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── private-policy.md ├── screenshots ├── kv │ ├── account-id.png │ ├── copy_token.png │ ├── create_namepace.png │ ├── create_token.png │ ├── created_token_list.png │ ├── custom_token.png │ ├── finish_create_token.png │ ├── input_name.png │ ├── namespaceId.png │ ├── paste.png │ ├── push_cookie.png │ ├── reload_page.png │ └── setting-permission.png ├── overview.png ├── overview_1280x800.png ├── panel.png ├── panel_1280 × 800.png ├── panel_item.png ├── settings.png ├── settings_1280 × 800.png ├── sync.png ├── sync_1280 × 800.png ├── sync_1400 × 560.png └── sync_440x280.png └── turbo.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | tailwind.config.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:import/recommended", 13 | "plugin:jsx-a11y/recommended", 14 | "prettier" 15 | ], 16 | "parser": "@typescript-eslint/parser", 17 | "parserOptions": { 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": "latest", 22 | "sourceType": "module" 23 | }, 24 | "plugins": ["react", "@typescript-eslint", "react-hooks", "import", "jsx-a11y", "prettier"], 25 | "settings": { 26 | "react": { 27 | "version": "detect" 28 | } 29 | }, 30 | "rules": { 31 | "prettier/prettier": "error", 32 | "react/react-in-jsx-scope": "off", 33 | "import/no-unresolved": "off", 34 | "jsx-a11y/click-events-have-key-events": "warning" 35 | }, 36 | "globals": { 37 | "chrome": "readonly" 38 | }, 39 | "ignorePatterns": ["watch.js", "dist/**"] 40 | } 41 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jackluson 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jackluson 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: jackluson 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Mac, Window, Linux] 28 | - Browser [e.g. chrome, firefox] 29 | - Node Version [e.g. 18.12.0] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: jackluson 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: author 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - jackluson 10 | 11 | # A number of reviewers added to the pull request 12 | # Set 0 to add all the reviewers (default: 0) 13 | numberOfReviewers: 0 14 | 15 | # A list of assignees, overrides reviewers if set 16 | # assignees: 17 | # - assigneeA 18 | 19 | # A number of assignees to add to the pull request 20 | # Set to 0 to add all of the assignees. 21 | # Uses numberOfReviewers if unset. 22 | # numberOfAssignees: 2 23 | 24 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 25 | # skipKeywords: 26 | # - wip 27 | 28 | filterLabels: 29 | exclude: 30 | - dependencies 31 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an Issue or Pull Request becomes stale 2 | daysUntilStale: 90 3 | # Number of days of inactivity before a stale Issue or Pull Request is closed 4 | daysUntilClose: 30 5 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking as stale 10 | staleLabel: stale 11 | # Comment to post when marking as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when removing the stale label. Set to `false` to disable 17 | unmarkComment: false 18 | # Comment to post when closing a stale Issue or Pull Request. Set to `false` to disable 19 | closeComment: true 20 | # Limit to only `issues` or `pulls` 21 | only: issues 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: 'Auto Assign' 2 | on: 3 | pull_request: 4 | types: [opened, ready_for_review] 5 | 6 | jobs: 7 | add-reviews: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: kentaro-m/auto-assign-action@v1.2.5 11 | with: 12 | configuration-path: '.github/auto_assign.yml' 13 | -------------------------------------------------------------------------------- /.github/workflows/build-zip.yml: -------------------------------------------------------------------------------- 1 | name: Build And Upload Extension Zip Via Artifact 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version-file: ".nvmrc" 20 | 21 | - uses: actions/cache@v3 22 | with: 23 | path: node_modules 24 | key: ${{ runner.OS }}-build-${{ hashFiles('**/pnpm-lock.yaml') }} 25 | 26 | - uses: pnpm/action-setup@v4 27 | 28 | - run: pnpm install --frozen-lockfile 29 | 30 | - run: pnpm build 31 | 32 | - uses: actions/upload-artifact@v4 33 | with: 34 | path: dist/* 35 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | issue-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' 16 | pr-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | **/node_modules 3 | 4 | # testing 5 | **/coverage 6 | 7 | # build 8 | **/dist 9 | **/dist-zip 10 | **/build 11 | 12 | # env 13 | **/.env.local 14 | **/.env 15 | 16 | # etc 17 | .DS_Store 18 | .idea 19 | **/.turbo 20 | 21 | # compiled 22 | apps/chrome-extension/public/manifest.json 23 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=@testing-library/dom 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.13.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | proto 4 | .gitignore 5 | .github 6 | .eslintignore 7 | .husky 8 | .nvmrc 9 | .prettierignore 10 | LICENSE 11 | *.md 12 | pnpm-lock.yaml 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": true, 4 | "singleQuote": true, 5 | "arrowParens": "avoid", 6 | "printWidth": 120, 7 | "bracketSameLine": true, 8 | "htmlWhitespaceSensitivity": "strict" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "typescript", 6 | "typescriptreact" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.formatDocument": "explicit", 10 | "source.fixAll.eslint": "explicit", 11 | "source.organizeImports": "explicit" 12 | }, 13 | "files.insertFinalNewline": true, 14 | "deepscan.enable": true 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Jack Lu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <div align="center"> 2 | <img src="chrome-extension/public/icon-128.png" alt="logo"/> 3 | <h1> Sync your cookie to Your Cloudflare</h1> 4 | 5 |  6 |  7 |  8 |  9 | <!-- <img src="https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https://github.com/jackluson/sync-your-cookieFactions&count_bg=%23#222222&title_bg=%23#454545&title=😀&edge_flat=true" alt="hits"/> --> 10 | 11 | </div> 12 | 13 | [English](./README.md) | [中文](./README_ZH.md) 14 | 15 | `Sync your cookie` is a chrome extension that helps you to sync your cookie to Cloudflare. It's a useful tool for web developers to share cookies between different devices. 16 | 17 | ### Install 18 | [Sync Your Cookie](https://chromewebstore.google.com/detail/sync-your-cookie/bcegpckmgklcpcapnbigfdadedcneopf) 19 | 20 | 21 | ### Features 22 | 23 | - Supports syncing cookies to Cloudflare 24 | - Supports configuring `Auto Merge` and `Auto Push` rules for different sites 25 | - Cookie data is transmitted via protobuf encoding 26 | - Provides a management panel to facilitate viewing, copying, and managing synchronized cookie data 27 | 28 | 29 | ### Project Screenshots 30 | 31 | Account Settings Page 32 | 33 | <img width="600" src="./screenshots/settings.png" alt="account settings"/> 34 | 35 | Cookie Sync Popup Page 36 | 37 | <img width="600" src="./screenshots/sync.png" alt="cookie sync popup"/> 38 | 39 | Cookie Manager Sidebar Panel 40 | 41 | <img width="600" src="./screenshots/panel.png" alt="cookie manager sidebar panel"/> 42 | 43 | Cookie Detail 44 | 45 | <img width="600" src="./screenshots/panel_item.png" alt="cookie manager sidebar panel"/> 46 | 47 | 48 | 49 | ### Usage 50 | 51 | [How to use](./how-to-use.md) 52 | 53 | ### TODO 54 | 55 | - [x] Custom Store Configure 56 | - [x] Multi-account synchronization based on Storage-key 57 | - [ ] Sync LocalStorage 58 | - [ ] More Cloud Platform (First github gist) 59 | 60 | ### Privacy Policy 61 | 62 | Please refer to [Privacy Policy](./private-policy.md) for more information. 63 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | <div align="center"> 2 | <img src="chrome-extension/public/icon-128.png" alt="logo"/> 3 | <h1> Sync your cookie to <br/>Cloudflare</h1> 4 | </div> 5 | 6 | [English](./README.md) | [中文](./README_ZH.md) 7 | 8 | `Sync your cookie` 是一个 Chrome 扩展程序,它可以帮助您将 Cookie 同步到 Cloudflare。它是一个有用的工具,用于在不同设备之间共享 Cookie, 免去了登录流程的烦恼,此外也提供了cookie管理面板查看,管理已经过同步的 cookie。 9 | 10 | 11 | <!-- // 使用gif展示功能 --> 12 | 13 | ### 功能 14 | - 支持同步 Cookie 到 Cloudflare 15 | - 支持为不同站点配置`Auto Merge`和`Auto Push`规则 16 | - Cookie数据经过 protobuf 编码传输 17 | - 提供了一个管理面板,方便查看、复制、管理已经同步的 Cookie 数据 18 | 19 | ### 项目截图 20 | 21 | 账号设置页面 22 | 23 | <img width="600" src="./screenshots/settings.png" alt="account settings"/> 24 | 25 | Cookie 同步页面 26 | 27 | <img width="600" src="./screenshots/sync.png" alt="cookie sync popup"/> 28 | 29 | Cookie 管理侧边栏面板 30 | 31 | <img width="600" src="./screenshots/panel.png" alt="cookie manager sidebar panel"/> 32 | 33 | Cookie 详情 34 | 35 | <img width="600" src="./screenshots/panel_item.png" alt="cookie manager sidebar panel"/> 36 | 37 | 38 | 39 | ### 使用指引 40 | 41 | [How to use](./how-to-use.md) 42 | 43 | ### Privacy Policy 44 | 45 | Please refer to [Privacy Policy](./private-policy.md) for more information. 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /chrome-extension/lib/background/badge.ts: -------------------------------------------------------------------------------- 1 | export function setBadge(text: string, color: string = '#7246e4') { 2 | chrome.action.setBadgeText({ text }); 3 | chrome.action.setBadgeBackgroundColor({ color }); 4 | } 5 | 6 | export function clearBadge() { 7 | chrome.action.setBadgeText({ text: '' }); 8 | } 9 | 10 | export function setPullingBadge() { 11 | setBadge('↓'); 12 | } 13 | 14 | export function setPushingBadge() { 15 | setBadge('↑'); 16 | } 17 | 18 | export function setPushingAndPullingBadge() { 19 | // badge('↓↑'); 20 | setBadge('⇅'); 21 | } 22 | -------------------------------------------------------------------------------- /chrome-extension/lib/background/index.ts: -------------------------------------------------------------------------------- 1 | // sort-imports-ignore 2 | import 'webextension-polyfill'; 3 | 4 | import { 5 | extractDomainAndPort, 6 | pullAndSetCookies, 7 | pullCookies, 8 | pushMultipleDomainCookies, 9 | } from '@sync-your-cookie/shared'; 10 | import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; 11 | import { refreshListen } from './listen'; 12 | import { initSubscribe } from './subscribe'; 13 | 14 | const init = async () => { 15 | try { 16 | await refreshListen(); 17 | console.log('initListen finish'); 18 | await initSubscribe(); // await state reset finish 19 | console.log('initSubscribe finish'); 20 | await pullCookies(true); 21 | console.log('init pullCookies finish'); 22 | } catch (error) { 23 | console.log('init-->error', error); 24 | } 25 | }; 26 | 27 | chrome.runtime.onInstalled.addListener(async () => { 28 | init(); 29 | // chrome.sidePanel.setPanexlBehavior({ openPanelOnActionClick: false }); 30 | chrome.contextMenus.create({ 31 | id: 'openSidePanel', 32 | title: 'Open Cookie Manager', 33 | contexts: ['all'], 34 | }); 35 | 36 | chrome.contextMenus.onClicked.addListener((info, tab) => { 37 | if (info.menuItemId === 'openSidePanel' && tab?.windowId) { 38 | // This will open the panel in all the pages on the current window. 39 | chrome.sidePanel.open({ windowId: tab.windowId }); 40 | } 41 | }); 42 | }); 43 | 44 | let delayTimer: NodeJS.Timeout | null = null; 45 | let checkDelayTimer: NodeJS.Timeout | null = null; 46 | let timeoutFlag = false; 47 | const changedDomainSet = new Set<string>(); 48 | chrome.cookies.onChanged.addListener(async changeInfo => { 49 | const domainConfigSnapShot = await domainConfigStorage.getSnapshot(); 50 | const domain = changeInfo.cookie.domain; 51 | const domainMap = domainConfigSnapShot?.domainMap || {}; 52 | let flag = false; 53 | for (const key in domainMap) { 54 | if (domain.endsWith(key) && domainMap[key]?.autoPush) { 55 | flag = true; 56 | break; 57 | } 58 | } 59 | if (!flag) return; 60 | if (delayTimer && timeoutFlag) { 61 | return; 62 | } 63 | delayTimer && clearTimeout(delayTimer); 64 | changedDomainSet.add(domain); 65 | delayTimer = setTimeout(async () => { 66 | timeoutFlag = false; 67 | if (checkDelayTimer) { 68 | clearTimeout(checkDelayTimer); 69 | } 70 | const domainConfig = await domainConfigStorage.get(); 71 | const pushDomainSet = new Set<string>(); 72 | for (const domain of changedDomainSet) { 73 | for (const key in domainConfig.domainMap) { 74 | if (domain.endsWith(key) && domainConfig.domainMap[key]?.autoPush) { 75 | pushDomainSet.add(key); 76 | } 77 | } 78 | } 79 | 80 | const uploadDomainCookies = []; 81 | for (const host of pushDomainSet) { 82 | const [domain] = await extractDomainAndPort(host); 83 | 84 | const cookies = await chrome.cookies.getAll({ 85 | domain: domain, 86 | }); 87 | uploadDomainCookies.push({ 88 | domain: host, 89 | cookies, 90 | }); 91 | } 92 | if (uploadDomainCookies.length) { 93 | await pushMultipleDomainCookies(uploadDomainCookies); 94 | changedDomainSet.clear(); 95 | } 96 | }, 15000); 97 | 98 | if (!checkDelayTimer) { 99 | checkDelayTimer = setTimeout(() => { 100 | if (delayTimer) { 101 | console.info('checkDelayTimer timeout'); 102 | timeoutFlag = true; 103 | } 104 | checkDelayTimer = null; 105 | }, 60000); 106 | } 107 | }); 108 | 109 | let previousActiveTabList: chrome.tabs.Tab[] = []; 110 | 111 | chrome.tabs.onUpdated.addListener(async function (tabId, changeInfo, tab) { 112 | // 1. current tab not exist in the tabMap 113 | // read changeInfo data and do something with it (like read the url) 114 | if (changeInfo.status === 'loading' && changeInfo.url) { 115 | const domainConfig = await domainConfigStorage.get(); 116 | let pullDomain = ''; 117 | let needPull = false; 118 | for (const key in domainConfig.domainMap) { 119 | if (new URL(changeInfo.url).host.endsWith(key) && domainConfig.domainMap[key]?.autoPull) { 120 | needPull = true; 121 | pullDomain = key; 122 | break; 123 | // await pullCookies(); 124 | } 125 | } 126 | if (needPull) { 127 | const allOpendTabs = await chrome.tabs.query({}); 128 | const otherExistedTabs = allOpendTabs.filter(itemTab => tab.id !== itemTab.id); 129 | for (const itemTab of otherExistedTabs) { 130 | if (itemTab.url && new URL(itemTab.url).host === new URL(changeInfo.url).host) { 131 | needPull = false; 132 | break; 133 | } 134 | } 135 | } 136 | 137 | if (needPull) { 138 | for (const itemTab of previousActiveTabList) { 139 | if (itemTab.url && new URL(itemTab.url).host === new URL(changeInfo.url).host) { 140 | needPull = false; 141 | break; 142 | } 143 | } 144 | } 145 | if (needPull) { 146 | await pullAndSetCookies(changeInfo.url, pullDomain); 147 | } 148 | const allActiveTabs = await chrome.tabs.query({ 149 | active: true, 150 | }); 151 | previousActiveTabList = allActiveTabs; 152 | } 153 | }); 154 | 155 | // let previousUrl = ''; 156 | // chrome.webNavigation?.onBeforeNavigate.addListener(function (object) { 157 | // chrome.tabs.get(object.tabId, function (tab) { 158 | // previousUrl = tab.url || ''; 159 | // console.log('previousUrl', previousUrl); 160 | // }); 161 | // }); 162 | 163 | // chrome.tabs.onRemoved.addListener(async function (tabId, removeInfo) { 164 | // const allActiveTabs = await chrome.tabs.query({ 165 | // active: true, 166 | // }); 167 | // previousActiveTabList = allActiveTabs; 168 | // }); 169 | 170 | chrome.tabs.onActivated.addListener(async function () { 171 | const allActiveTabs = await chrome.tabs.query({ 172 | active: true, 173 | }); 174 | previousActiveTabList = allActiveTabs; 175 | refreshListen(); 176 | }); 177 | -------------------------------------------------------------------------------- /chrome-extension/lib/background/listen.ts: -------------------------------------------------------------------------------- 1 | import { 2 | check, 3 | checkCloudflareResponse, 4 | CookieOperator, 5 | extractDomainAndPort, 6 | ICookie, 7 | Message, 8 | MessageType, 9 | pullAndSetCookies, 10 | PushCookieMessagePayload, 11 | pushCookies, 12 | removeCookieItem, 13 | removeCookies, 14 | SendResponse, 15 | } from '@sync-your-cookie/shared'; 16 | 17 | import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; 18 | import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; 19 | 20 | type HandleCallback = (response?: SendResponse) => void; 21 | 22 | const handlePush = async (payload: PushCookieMessagePayload, callback: HandleCallback) => { 23 | const { sourceUrl, host, favIconUrl } = payload || {}; 24 | try { 25 | await check(); 26 | await domainConfigStorage.updateItem(host, { 27 | sourceUrl: sourceUrl, 28 | favIconUrl, 29 | }); 30 | await domainStatusStorage.updateItem(host, { 31 | pushing: true, 32 | }); 33 | const [domain] = await extractDomainAndPort(host); 34 | const cookies = await chrome.cookies.getAll({ 35 | // url: activeTabUrl, 36 | domain: domain, 37 | }); 38 | if (cookies?.length) { 39 | const res = await pushCookies(host, cookies); 40 | checkCloudflareResponse(res, 'push', callback); 41 | } else { 42 | callback({ isOk: false, msg: 'no cookies found', result: cookies }); 43 | } 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | } catch (err: any) { 46 | checkCloudflareResponse(err, 'push', callback); 47 | } finally { 48 | await domainStatusStorage.togglePushingState(host, false); 49 | } 50 | }; 51 | 52 | const handlePull = async (activeTabUrl: string, domain: string, isReload: boolean, callback: HandleCallback) => { 53 | try { 54 | await check(); 55 | await domainStatusStorage.togglePullingState(domain, true); 56 | const cookieMap = await pullAndSetCookies(activeTabUrl, domain, isReload); 57 | callback({ isOk: true, msg: 'Pull success', result: cookieMap }); 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 | } catch (err: any) { 60 | checkCloudflareResponse(err, 'pull', callback); 61 | } finally { 62 | await domainStatusStorage.togglePullingState(domain, false); 63 | } 64 | }; 65 | 66 | const handleRemove = async (domain: string, callback: HandleCallback) => { 67 | try { 68 | await check(); 69 | const res = await removeCookies(domain); 70 | if (res.success) { 71 | callback({ isOk: true, msg: 'Removed success' }); 72 | } else { 73 | checkCloudflareResponse(res, 'remove', callback); 74 | // callback({ isOk: false, msg: 'Removed fail, please try again ', result: res }); 75 | } 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | } catch (err: any) { 78 | checkCloudflareResponse(err, 'remove', callback); 79 | 80 | // callback({ isOk: false, msg: (err as Error).message || 'remove fail, please try again ', result: err }); 81 | } 82 | }; 83 | 84 | const handleRemoveItem = async (domain: string, id: string, callback: HandleCallback) => { 85 | try { 86 | await check(); 87 | const res = await removeCookieItem(domain, id); 88 | if (res.success) { 89 | callback({ isOk: true, msg: 'Deleted success' }); 90 | } else { 91 | checkCloudflareResponse(res, 'delete', callback); 92 | } 93 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 94 | } catch (err: any) { 95 | checkCloudflareResponse(err, 'delete', callback); 96 | } 97 | }; 98 | 99 | const handleEditItem = async (domain: string, oldItem: ICookie, newItem: ICookie, callback: HandleCallback) => { 100 | try { 101 | await check(); 102 | const res = await CookieOperator.editCookieItem(domain, oldItem, newItem); 103 | if (res.success) { 104 | callback({ isOk: true, msg: 'Edited success' }); 105 | } else { 106 | checkCloudflareResponse(res, 'edit', callback); 107 | } 108 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 109 | } catch (err: any) { 110 | checkCloudflareResponse(err, 'edit', callback); 111 | } 112 | }; 113 | function handleMessage( 114 | message: Message, 115 | sender: chrome.runtime.MessageSender, 116 | callback: (response?: SendResponse) => void, 117 | ) { 118 | const type = message.type; 119 | switch (type) { 120 | case MessageType.PushCookie: 121 | handlePush(message.payload, callback); 122 | break; 123 | case MessageType.PullCookie: 124 | // eslint-disable-next-line no-case-declarations, @typescript-eslint/no-non-null-asserted-optional-chain 125 | const activeTabUrl = message.payload.activeTabUrl || sender.tab?.url!; 126 | handlePull(activeTabUrl!, message.payload.domain, message.payload.reload, callback); 127 | break; 128 | case MessageType.RemoveCookie: 129 | handleRemove(message.payload.domain, callback); 130 | break; 131 | case MessageType.RemoveCookieItem: 132 | handleRemoveItem(message.payload.domain, message.payload.id, callback); 133 | break; 134 | case MessageType.EditCookieItem: 135 | handleEditItem(message.payload.domain, message.payload.oldItem, message.payload.newItem, callback); 136 | break; 137 | default: 138 | break; 139 | } 140 | return true; 141 | } 142 | export const refreshListen = async () => { 143 | chrome.runtime.onMessage.removeListener(handleMessage); 144 | chrome.runtime.onMessage.addListener(handleMessage); 145 | }; 146 | -------------------------------------------------------------------------------- /chrome-extension/lib/background/subscribe.ts: -------------------------------------------------------------------------------- 1 | import { cloudflareStorage } from '@sync-your-cookie/storage/lib/cloudflareStorage'; 2 | import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; 3 | 4 | import { pullCookies } from '@sync-your-cookie/shared'; 5 | import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; 6 | import { clearBadge, setPullingBadge, setPushingAndPullingBadge, setPushingBadge } from './badge'; 7 | 8 | export const initSubscribe = async () => { 9 | await domainStatusStorage.resetState(); 10 | domainStatusStorage.subscribe(async () => { 11 | const domainStatus = await domainStatusStorage.get(); 12 | if (domainStatus?.pulling && domainStatus.pushing) { 13 | setPushingAndPullingBadge(); 14 | } else if (domainStatus?.pushing) { 15 | setPushingBadge(); 16 | } else if (domainStatus?.pulling) { 17 | setPullingBadge(); 18 | } else { 19 | clearBadge(); 20 | } 21 | }); 22 | 23 | cloudflareStorage.subscribe(async () => { 24 | await domainStatusStorage.resetState(); 25 | await cookieStorage.reset(); 26 | await pullCookies(); 27 | console.log("reset finished"); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /chrome-extension/manifest.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | const packageJson = JSON.parse(fs.readFileSync('../package.json', 'utf8')); 4 | 5 | const isFirefox = process.env.__FIREFOX__ === 'true'; 6 | 7 | const sidePanelConfig = { 8 | side_panel: { 9 | default_path: 'sidepanel/index.html', 10 | }, 11 | permissions: !isFirefox ? ['sidePanel', 'contextMenus'] : [], 12 | }; 13 | 14 | /** 15 | * After changing, please reload the extension at `chrome://extensions` 16 | * @type {chrome.runtime.ManifestV3} 17 | */ 18 | const manifest = Object.assign( 19 | { 20 | manifest_version: 3, 21 | default_locale: 'en', 22 | /** 23 | * if you want to support multiple languages, you can use the following reference 24 | * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Internationalization 25 | */ 26 | name: 'Sync Your Cookie', 27 | version: packageJson.version, 28 | description: 'A browser extension that syncs and manages your cookies to your cloudflare', 29 | permissions: ['cookies', 'tabs', 'storage'].concat(sidePanelConfig.permissions), 30 | host_permissions: ['<all_urls>'], 31 | options_page: 'options/index.html', 32 | background: { 33 | service_worker: 'background.iife.js', 34 | type: 'module', 35 | }, 36 | action: { 37 | default_popup: 'popup/index.html', 38 | default_icon: 'icon-34.png', 39 | }, 40 | // chrome_url_overrides: { 41 | // newtab: 'newtab/index.html', 42 | // }, 43 | icons: { 44 | 128: 'icon-128.png', 45 | }, 46 | // devtools_page: 'devtools/index.html', 47 | web_accessible_resources: [ 48 | { 49 | resources: ['*.js', '*.css', '*.svg', 'icon-128.png', 'icon-34.png'], 50 | matches: ['*://*/*'], 51 | }, 52 | ], 53 | }, 54 | !isFirefox && { side_panel: { ...sidePanelConfig.side_panel } }, 55 | ); 56 | 57 | export default manifest; 58 | -------------------------------------------------------------------------------- /chrome-extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-extension", 3 | "version": "1.0.1", 4 | "description": "chrome extension", 5 | "scripts": { 6 | "clean": "rimraf ../../dist && rimraf .turbo", 7 | "build": "tsc --noEmit && vite build", 8 | "build:firefox": "tsc --noEmit && cross-env __FIREFOX__=true vite build", 9 | "build:watch": "cross-env __DEV__=true vite build -w --mode development", 10 | "build:firefox:watch": "cross-env __DEV__=true __FIREFOX__=true vite build -w --mode development", 11 | "dev": "pnpm build:watch", 12 | "dev:firefox": "pnpm build:firefox:watch", 13 | "test": "vitest run", 14 | "lint": "eslint ./ --ext .ts,.js,.tsx,.jsx", 15 | "lint:fix": "pnpm lint --fix", 16 | "prettier": "prettier . --write", 17 | "type-check": "tsc --noEmit" 18 | }, 19 | "type": "module", 20 | "dependencies": { 21 | "webextension-polyfill": "^0.12.0", 22 | "@sync-your-cookie/shared": "workspace:*", 23 | "@sync-your-cookie/storage": "workspace:*" 24 | }, 25 | "devDependencies": { 26 | "@sync-your-cookie/dev-utils": "workspace:*", 27 | "@sync-your-cookie/hmr": "workspace:*", 28 | "@sync-your-cookie/tsconfig": "workspace:*", 29 | "@laynezh/vite-plugin-lib-assets": "^0.5.21", 30 | "@types/ws": "^8.5.10", 31 | "magic-string": "^0.30.10", 32 | "ts-loader": "^9.5.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chrome-extension/public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionDescription": { 3 | "description": "sync your cookie free", 4 | "message": "Sync your cookies to cloudlfare " 5 | }, 6 | "extensionName": { 7 | "description": "Sync your cookie", 8 | "message": "Sync your cookie" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /chrome-extension/public/content.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/chrome-extension/public/content.css -------------------------------------------------------------------------------- /chrome-extension/public/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/chrome-extension/public/icon-128.png -------------------------------------------------------------------------------- /chrome-extension/public/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/chrome-extension/public/icon-34.png -------------------------------------------------------------------------------- /chrome-extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/app.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "types": ["vite/client", "node", "chrome"], 8 | "paths": { 9 | "@root/*": ["./*"], 10 | "@lib/*": ["lib/*"] 11 | } 12 | }, 13 | "include": ["lib", "utils", "vite.config.ts", "node_modules/@types"] 14 | } 15 | -------------------------------------------------------------------------------- /chrome-extension/utils/plugins/make-manifest-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { ManifestParser, colorLog } from '@sync-your-cookie/dev-utils'; 4 | import type { PluginOption } from 'vite'; 5 | import { pathToFileURL } from 'url'; 6 | import * as process from 'process'; 7 | 8 | const { resolve } = path; 9 | 10 | const rootDir = resolve(__dirname, '..', '..'); 11 | const manifestFile = resolve(rootDir, 'manifest.js'); 12 | 13 | const getManifestWithCacheBurst = (): Promise<{ default: chrome.runtime.ManifestV3 }> => { 14 | const withCacheBurst = (path: string) => `${path}?${Date.now().toString()}`; 15 | /** 16 | * In Windows, import() doesn't work without file:// protocol. 17 | * So, we need to convert path to file:// protocol. (url.pathToFileURL) 18 | */ 19 | if (process.platform === 'win32') { 20 | return import(withCacheBurst(pathToFileURL(manifestFile).href)); 21 | } 22 | return import(withCacheBurst(manifestFile)); 23 | }; 24 | 25 | export default function makeManifestPlugin(config: { outDir: string }): PluginOption { 26 | function makeManifest(manifest: chrome.runtime.ManifestV3, to: string) { 27 | if (!fs.existsSync(to)) { 28 | fs.mkdirSync(to); 29 | } 30 | const manifestPath = resolve(to, 'manifest.json'); 31 | 32 | const isFirefox = process.env.__FIREFOX__; 33 | fs.writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, isFirefox ? 'firefox' : 'chrome')); 34 | 35 | colorLog(`Manifest file copy complete: ${manifestPath}`, 'success'); 36 | } 37 | 38 | return { 39 | name: 'make-manifest', 40 | buildStart() { 41 | this.addWatchFile(manifestFile); 42 | }, 43 | async writeBundle() { 44 | const outDir = config.outDir; 45 | const manifest = await getManifestWithCacheBurst(); 46 | makeManifest(manifest.default, outDir); 47 | }, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /chrome-extension/vite.config.ts: -------------------------------------------------------------------------------- 1 | import libAssetsPlugin from '@laynezh/vite-plugin-lib-assets'; 2 | import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; 3 | import { resolve } from 'path'; 4 | import { defineConfig } from 'vite'; 5 | import makeManifestPlugin from './utils/plugins/make-manifest-plugin'; 6 | 7 | const rootDir = resolve(__dirname); 8 | const libDir = resolve(rootDir, 'lib'); 9 | 10 | const isDev = process.env.__DEV__ === 'true'; 11 | const isProduction = !isDev; 12 | 13 | const outDir = resolve(rootDir, '..', 'dist'); 14 | export default defineConfig({ 15 | resolve: { 16 | alias: { 17 | '@root': rootDir, 18 | '@lib': libDir, 19 | '@assets': resolve(libDir, 'assets'), 20 | }, 21 | }, 22 | define: { 23 | 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, 24 | }, 25 | plugins: [ 26 | libAssetsPlugin({ 27 | outputPath: outDir, 28 | }), 29 | makeManifestPlugin({ outDir }), 30 | isDev && watchRebuildPlugin({ reload: true }), 31 | ], 32 | publicDir: resolve(rootDir, 'public'), 33 | build: { 34 | lib: { 35 | formats: ['iife'], 36 | entry: resolve(__dirname, 'lib/background/index.ts'), 37 | name: 'BackgroundScript', 38 | fileName: 'background', 39 | }, 40 | outDir, 41 | sourcemap: isDev, 42 | minify: isProduction, 43 | reportCompressedSize: isProduction, 44 | modulePreload: true, 45 | rollupOptions: { 46 | external: ['chrome'], 47 | output: { 48 | inlineDynamicImports: true, 49 | }, 50 | }, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /googlef759ff453695209f.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlef759ff453695209f.html -------------------------------------------------------------------------------- /how-to-use.md: -------------------------------------------------------------------------------- 1 | 2 | ## How to use 3 | `Sync-Your-Cookie` uses Cloudflare [KV](https://developers.cloudflare.com/kv/) to store cookie data. Here is a tutorial on how to configure KV and Token: 4 | 5 | ## Create Namespace 6 | 7 |  8 | 9 | Input 10 |  11 | 12 | Your NamespaceId 13 |  14 | 15 | ## Your AccountId 16 | 17 |  18 | 19 | ## Create Token 20 | 21 | 1. Enter Profile Page 22 | 23 |  24 | 25 | 2. Custom Permission 26 | 27 |  28 | 29 | 3. Select KV Read and Write Permission 30 | 31 |  32 | 33 | 4. Confirm Create 34 | 35 |  36 | 37 | 5. Copy Token 38 | 39 |  40 | 41 | 6. Your Token List 42 | 43 |  44 | 45 | 7. Paste Your Account Info And Save 46 | 47 |  48 | 49 | 8. Push Your Cookie 50 | 51 |  52 | 53 | 9. Check Your Cookie 54 | 55 | The uploaded cookie is a protobuf-encoded string 56 |  57 | 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sync-your-cookie", 3 | "version": "1.0.1", 4 | "description": "sync your cookie extension monorepo", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jackluson/sync-your-cookie.git" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist && rimraf .turbo && turbo clean", 12 | "build": "turbo build", 13 | "build:firefox": "cross-env __FIREFOX__=true turbo build", 14 | "dev-server": "pnpm -F hmr build && pnpm -F hmr dev-server", 15 | "dev:apps": "cross-env __DEV__=true turbo run dev --filter=./pages/* --filter=chrome-extension --filter=@sync-your-cookie/hmr --concurrency 15", 16 | "dev": "cross-env __DEV__=true turbo run dev --concurrency 25", 17 | "dev:firefox": "cross-env __DEV__=true __FIREFOX__=true turbo dev --concurrency 20", 18 | "zip": "pnpm build && pnpm -F zipper zip", 19 | "test": "turbo test", 20 | "type-check": "turbo type-check", 21 | "lint": "turbo lint", 22 | "lint:fix": "turbo lint:fix", 23 | "prettier": "turbo prettier" 24 | }, 25 | "type": "module", 26 | "dependencies": { 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0" 29 | }, 30 | "devDependencies": { 31 | "@types/chrome": "^0.0.268", 32 | "@types/node": "^20.12.11", 33 | "@types/react": "^18.3.2", 34 | "@types/react-dom": "^18.3.0", 35 | "@typescript-eslint/eslint-plugin": "^6.21.0", 36 | "@typescript-eslint/parser": "^6.21.0", 37 | "@vitejs/plugin-react-swc": "^3.6.0", 38 | "autoprefixer": "^10.4.19", 39 | "concurrently": "^8.2.2", 40 | "cross-env": "^7.0.3", 41 | "eslint": "8.56.0", 42 | "eslint-config-airbnb-typescript": "17.1.0", 43 | "eslint-config-prettier": "9.0.0", 44 | "eslint-plugin-import": "2.29.1", 45 | "eslint-plugin-jsx-a11y": "6.8.0", 46 | "eslint-plugin-prettier": "5.1.3", 47 | "eslint-plugin-react": "7.33.2", 48 | "eslint-plugin-react-hooks": "4.6.2", 49 | "postcss": "^8.4.38", 50 | "prettier": "^3.2.5", 51 | "rimraf": "^5.0.7", 52 | "tailwindcss": "^3.4.3", 53 | "tslib": "^2.6.2", 54 | "turbo": "^2.0.3", 55 | "typescript": "5.2.2", 56 | "vite": "^5.2.11" 57 | }, 58 | "packageManager": "pnpm@9.1.1", 59 | "engines": { 60 | "node": ">=20.12.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/dev-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/manifest-parser'; 2 | export * from './lib/logger'; 3 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/logger.ts: -------------------------------------------------------------------------------- 1 | type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS; 2 | type ValueOf<T> = T[keyof T]; 3 | 4 | export function colorLog(message: string, type: ColorType) { 5 | let color: ValueOf<typeof COLORS>; 6 | 7 | switch (type) { 8 | case 'success': 9 | color = COLORS.FgGreen; 10 | break; 11 | case 'info': 12 | color = COLORS.FgBlue; 13 | break; 14 | case 'error': 15 | color = COLORS.FgRed; 16 | break; 17 | case 'warning': 18 | color = COLORS.FgYellow; 19 | break; 20 | default: 21 | color = COLORS[type]; 22 | break; 23 | } 24 | 25 | console.log(color, message); 26 | } 27 | 28 | const COLORS = { 29 | Reset: '\x1b[0m', 30 | Bright: '\x1b[1m', 31 | Dim: '\x1b[2m', 32 | Underscore: '\x1b[4m', 33 | Blink: '\x1b[5m', 34 | Reverse: '\x1b[7m', 35 | Hidden: '\x1b[8m', 36 | FgBlack: '\x1b[30m', 37 | FgRed: '\x1b[31m', 38 | FgGreen: '\x1b[32m', 39 | FgYellow: '\x1b[33m', 40 | FgBlue: '\x1b[34m', 41 | FgMagenta: '\x1b[35m', 42 | FgCyan: '\x1b[36m', 43 | FgWhite: '\x1b[37m', 44 | BgBlack: '\x1b[40m', 45 | BgRed: '\x1b[41m', 46 | BgGreen: '\x1b[42m', 47 | BgYellow: '\x1b[43m', 48 | BgBlue: '\x1b[44m', 49 | BgMagenta: '\x1b[45m', 50 | BgCyan: '\x1b[46m', 51 | BgWhite: '\x1b[47m', 52 | } as const; 53 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/manifest-parser/impl.ts: -------------------------------------------------------------------------------- 1 | import { ManifestParserInterface, Manifest } from './type'; 2 | 3 | export const ManifestParserImpl: ManifestParserInterface = { 4 | convertManifestToString: (manifest, env) => { 5 | if (env === 'firefox') { 6 | manifest = convertToFirefoxCompatibleManifest(manifest); 7 | } 8 | return JSON.stringify(manifest, null, 2); 9 | }, 10 | }; 11 | 12 | function convertToFirefoxCompatibleManifest(manifest: Manifest) { 13 | const manifestCopy = { 14 | ...manifest, 15 | } as { [key: string]: unknown }; 16 | 17 | manifestCopy.background = { 18 | scripts: [manifest.background?.service_worker], 19 | type: 'module', 20 | }; 21 | manifestCopy.options_ui = { 22 | page: manifest.options_page, 23 | browser_style: false, 24 | }; 25 | manifestCopy.content_security_policy = { 26 | extension_pages: "script-src 'self'; object-src 'self'", 27 | }; 28 | delete manifestCopy.options_page; 29 | return manifestCopy as Manifest; 30 | } 31 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/manifest-parser/index.ts: -------------------------------------------------------------------------------- 1 | import { ManifestParserImpl } from './impl'; 2 | export const ManifestParser = ManifestParserImpl; 3 | -------------------------------------------------------------------------------- /packages/dev-utils/lib/manifest-parser/type.ts: -------------------------------------------------------------------------------- 1 | export type Manifest = chrome.runtime.ManifestV3; 2 | 3 | export interface ManifestParserInterface { 4 | convertManifestToString: (manifest: Manifest, env: 'chrome' | 'firefox') => string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/dev-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/dev-utils", 3 | "version": "0.0.1", 4 | "description": "chrome extension dev utils", 5 | "private": true, 6 | "sideEffects": false, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "main": "dist/index.js", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "scripts": { 14 | "clean": "rimraf ./dist && rimraf ./build && rimraf .turbo", 15 | "build": "pnpm run clean && tsc", 16 | "lint": "eslint . --ext .ts,.tsx", 17 | "lint:fix": "pnpm lint --fix", 18 | "prettier": "prettier . --write", 19 | "type-check": "tsc --noEmit" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "@sync-your-cookie/tsconfig": "workspace:*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/dev-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "types": ["chrome"] 6 | }, 7 | "include": ["index.ts", "lib"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/hmr/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/plugins'; 2 | -------------------------------------------------------------------------------- /packages/hmr/lib/constant.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_RELOAD_SOCKET_PORT = 8081; 2 | export const LOCAL_RELOAD_SOCKET_URL = `ws://localhost:${LOCAL_RELOAD_SOCKET_PORT}`; 3 | -------------------------------------------------------------------------------- /packages/hmr/lib/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce<A extends unknown[]>(callback: (...args: A) => void, delay: number) { 2 | let timer: NodeJS.Timeout; 3 | return function (...args: A) { 4 | clearTimeout(timer); 5 | timer = setTimeout(() => callback(...args), delay); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /packages/hmr/lib/initClient.ts: -------------------------------------------------------------------------------- 1 | import { LOCAL_RELOAD_SOCKET_URL } from './constant'; 2 | import MessageInterpreter from './interpreter'; 3 | 4 | export default function initReloadClient({ id, onUpdate }: { id: string; onUpdate: () => void }) { 5 | let ws: WebSocket | null = null; 6 | try { 7 | ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL); 8 | ws.onopen = () => { 9 | ws?.addEventListener('message', event => { 10 | const message = MessageInterpreter.receive(String(event.data)); 11 | if (message.type === 'ping') { 12 | console.log('[HMR] Client OK'); 13 | } 14 | if (message.type === 'do_update' && message.id === id) { 15 | sendUpdateCompleteMessage(); 16 | onUpdate(); 17 | return; 18 | } 19 | }); 20 | }; 21 | 22 | ws.onclose = () => { 23 | console.log( 24 | `Reload server disconnected.\nPlease check if the WebSocket server is running properly on ${LOCAL_RELOAD_SOCKET_URL}. This feature detects changes in the code and helps the browser to reload the extension or refresh the current tab.`, 25 | ); 26 | setTimeout(() => { 27 | initReloadClient({ onUpdate, id }); 28 | }, 1000); 29 | }; 30 | } catch (e) { 31 | setTimeout(() => { 32 | initReloadClient({ onUpdate, id }); 33 | }, 1000); 34 | } 35 | 36 | function sendUpdateCompleteMessage() { 37 | ws?.send(MessageInterpreter.send({ type: 'done_update' })); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/hmr/lib/initReloadServer.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { WebSocket, WebSocketServer } from 'ws'; 4 | import { LOCAL_RELOAD_SOCKET_PORT, LOCAL_RELOAD_SOCKET_URL } from './constant'; 5 | import MessageInterpreter from './interpreter'; 6 | 7 | const clientsThatNeedToUpdate: Set<WebSocket> = new Set(); 8 | 9 | function initReloadServer() { 10 | try { 11 | const wss = new WebSocketServer({ port: LOCAL_RELOAD_SOCKET_PORT }); 12 | 13 | wss.on('listening', () => console.log(`[HMR] Server listening at ${LOCAL_RELOAD_SOCKET_URL}`)); 14 | 15 | wss.on('connection', ws => { 16 | clientsThatNeedToUpdate.add(ws); 17 | 18 | ws.addEventListener('close', () => clientsThatNeedToUpdate.delete(ws)); 19 | ws.addEventListener('message', event => { 20 | if (typeof event.data !== 'string') return; 21 | 22 | const message = MessageInterpreter.receive(event.data); 23 | 24 | if (message.type === 'done_update') { 25 | ws.close(); 26 | } 27 | if (message.type === 'build_complete') { 28 | clientsThatNeedToUpdate.forEach((ws: WebSocket) => 29 | ws.send(MessageInterpreter.send({ type: 'do_update', id: message.id })), 30 | ); 31 | } 32 | }); 33 | }); 34 | 35 | ping(); 36 | } catch { 37 | console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`); 38 | console.error('PLEASE MAKE SURE YOU ARE RUNNING `pnpm dev-server`'); 39 | } 40 | } 41 | 42 | initReloadServer(); 43 | 44 | function ping() { 45 | clientsThatNeedToUpdate.forEach(ws => ws.send(MessageInterpreter.send({ type: 'ping' }))); 46 | setTimeout(() => { 47 | ping(); 48 | }, 15_000); 49 | } 50 | -------------------------------------------------------------------------------- /packages/hmr/lib/injections/refresh.ts: -------------------------------------------------------------------------------- 1 | import initClient from '../initClient'; 2 | 3 | function addRefresh() { 4 | let pendingReload = false; 5 | 6 | initClient({ 7 | // eslint-disable-next-line 8 | // @ts-ignore 9 | id: __HMR_ID, 10 | onUpdate: () => { 11 | // disable reload when tab is hidden 12 | if (document.hidden) { 13 | pendingReload = true; 14 | return; 15 | } 16 | reload(); 17 | }, 18 | }); 19 | 20 | // reload 21 | function reload(): void { 22 | pendingReload = false; 23 | window.location.reload(); 24 | } 25 | 26 | // reload when tab is visible 27 | function reloadWhenTabIsVisible(): void { 28 | !document.hidden && pendingReload && reload(); 29 | } 30 | document.addEventListener('visibilitychange', reloadWhenTabIsVisible); 31 | } 32 | 33 | addRefresh(); 34 | -------------------------------------------------------------------------------- /packages/hmr/lib/injections/reload.ts: -------------------------------------------------------------------------------- 1 | import initClient from '../initClient'; 2 | 3 | function addReload() { 4 | const reload = () => { 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | chrome.runtime.reload(); 8 | }; 9 | 10 | initClient({ 11 | // eslint-disable-next-line 12 | // @ts-ignore 13 | id: __HMR_ID, 14 | onUpdate: reload, 15 | }); 16 | } 17 | 18 | addReload(); 19 | -------------------------------------------------------------------------------- /packages/hmr/lib/interpreter/index.ts: -------------------------------------------------------------------------------- 1 | import type { WebSocketMessage, SerializedMessage } from './types'; 2 | 3 | export default class MessageInterpreter { 4 | // eslint-disable-next-line @typescript-eslint/no-empty-function 5 | private constructor() {} 6 | 7 | static send(message: WebSocketMessage): SerializedMessage { 8 | return JSON.stringify(message); 9 | } 10 | static receive(serializedMessage: SerializedMessage): WebSocketMessage { 11 | return JSON.parse(serializedMessage); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/hmr/lib/interpreter/types.ts: -------------------------------------------------------------------------------- 1 | type UpdateRequestMessage = { 2 | type: 'do_update'; 3 | id: string; 4 | }; 5 | type UpdateCompleteMessage = { type: 'done_update' }; 6 | type PingMessage = { type: 'ping' }; 7 | type BuildCompletionMessage = { type: 'build_complete'; id: string }; 8 | 9 | export type SerializedMessage = string; 10 | 11 | export type WebSocketMessage = UpdateCompleteMessage | UpdateRequestMessage | BuildCompletionMessage | PingMessage; 12 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './watch-rebuild-plugin'; 2 | export * from './make-entry-point-plugin'; 3 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/make-entry-point-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import path from 'path'; 3 | import type { PluginOption } from 'vite'; 4 | 5 | /** 6 | * make entry point file for content script cache busting 7 | */ 8 | 9 | export function makeEntryPointPlugin(): PluginOption { 10 | const cleanupTargets = new Set<string>(); 11 | const isFirefox = process.env.__FIREFOX__ === 'true'; 12 | 13 | return { 14 | name: 'make-entry-point-plugin', 15 | generateBundle(options, bundle) { 16 | const outputDir = options.dir; 17 | if (!outputDir) { 18 | throw new Error('Output directory not found'); 19 | } 20 | for (const module of Object.values(bundle)) { 21 | const fileName = path.basename(module.fileName); 22 | const newFileName = fileName.replace('.js', '_dev.js'); 23 | switch (module.type) { 24 | case 'asset': 25 | // map file 26 | if (fileName.endsWith('.map')) { 27 | cleanupTargets.add(path.resolve(outputDir, fileName)); 28 | const originalFileName = fileName.replace('.map', ''); 29 | const replacedSource = String(module.source).replaceAll(originalFileName, newFileName); 30 | module.source = ''; 31 | fs.writeFileSync(path.resolve(outputDir, newFileName), replacedSource); 32 | break; 33 | } 34 | break; 35 | case 'chunk': { 36 | fs.writeFileSync(path.resolve(outputDir, newFileName), module.code); 37 | console.log('newFileName', newFileName); 38 | if (isFirefox) { 39 | const contentDirectory = extractContentDir(outputDir); 40 | module.code = `import(browser.runtime.getURL("${contentDirectory}/${newFileName}"));`; 41 | } else { 42 | module.code = `import('./${newFileName}');`; 43 | } 44 | break; 45 | } 46 | } 47 | } 48 | }, 49 | closeBundle() { 50 | cleanupTargets.forEach(target => { 51 | fs.unlinkSync(target); 52 | }); 53 | }, 54 | }; 55 | } 56 | 57 | /** 58 | * Extract content directory from output directory for Firefox 59 | * @param outputDir 60 | */ 61 | function extractContentDir(outputDir: string) { 62 | const parts = outputDir.split(path.sep); 63 | const distIndex = parts.indexOf('dist'); 64 | if (distIndex !== -1 && distIndex < parts.length - 1) { 65 | return parts.slice(distIndex + 1); 66 | } 67 | throw new Error('Output directory does not contain "dist"'); 68 | } 69 | -------------------------------------------------------------------------------- /packages/hmr/lib/plugins/watch-rebuild-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from 'vite'; 2 | import { WebSocket } from 'ws'; 3 | import MessageInterpreter from '../interpreter'; 4 | import { LOCAL_RELOAD_SOCKET_URL } from '../constant'; 5 | import * as fs from 'fs'; 6 | import path from 'path'; 7 | 8 | type PluginConfig = { 9 | onStart?: () => void; 10 | reload?: boolean; 11 | refresh?: boolean; 12 | }; 13 | 14 | const injectionsPath = path.resolve(__dirname, '..', '..', '..', 'build', 'injections'); 15 | 16 | const refreshCode = fs.readFileSync(path.resolve(injectionsPath, 'refresh.js'), 'utf-8'); 17 | const reloadCode = fs.readFileSync(path.resolve(injectionsPath, 'reload.js'), 'utf-8'); 18 | 19 | export function watchRebuildPlugin(config: PluginConfig): PluginOption { 20 | let ws: WebSocket | null = null; 21 | const id = Math.random().toString(36); 22 | const { refresh, reload } = config; 23 | 24 | const hmrCode = (refresh ? refreshCode : '') + (reload ? reloadCode : ''); 25 | 26 | function initializeWebSocket() { 27 | if (!ws) { 28 | ws = new WebSocket(LOCAL_RELOAD_SOCKET_URL); 29 | ws.onopen = () => { 30 | console.log(`[HMR] Connected to dev-server at ${LOCAL_RELOAD_SOCKET_URL}`); 31 | }; 32 | ws.onerror = () => { 33 | console.error(`[HMR] Failed to start server at ${LOCAL_RELOAD_SOCKET_URL}`); 34 | console.error('PLEASE MAKE SURE YOU ARE RUNNING `pnpm dev-server`'); 35 | console.warn('Retrying in 5 seconds...'); 36 | ws = null; 37 | setTimeout(() => initializeWebSocket(), 5_000); 38 | }; 39 | } 40 | } 41 | 42 | return { 43 | name: 'watch-rebuild', 44 | writeBundle() { 45 | config.onStart?.(); 46 | if (!ws) { 47 | initializeWebSocket(); 48 | return; 49 | } 50 | /** 51 | * When the build is complete, send a message to the reload server. 52 | * The reload server will send a message to the client to reload or refresh the extension. 53 | */ 54 | if (!ws) { 55 | throw new Error('WebSocket is not initialized'); 56 | } 57 | ws.send(MessageInterpreter.send({ type: 'build_complete', id })); 58 | }, 59 | generateBundle(_options, bundle) { 60 | for (const module of Object.values(bundle)) { 61 | if (module.type === 'chunk') { 62 | module.code = `(function() {let __HMR_ID = "${id}";\n` + hmrCode + '\n' + '})();' + '\n' + module.code; 63 | } 64 | } 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/hmr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/hmr", 3 | "version": "0.0.1", 4 | "description": "chrome extension hot module reload or refresh", 5 | "private": true, 6 | "sideEffects": true, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "main": "dist/index.js", 11 | "module": "dist/index.js", 12 | "types": "dist/index.d.ts", 13 | "scripts": { 14 | "clean": "rimraf ./dist && rimraf ./build && rimraf .turbo", 15 | "build:tsc": "tsc -b tsconfig.build.json", 16 | "build:rollup": "rollup --config rollup.config.mjs", 17 | "build": "pnpm run build:tsc && pnpm run build:rollup", 18 | "dev": "node dist/lib/initReloadServer.js", 19 | "lint": "eslint . --ext .ts,.tsx", 20 | "lint:fix": "pnpm lint --fix", 21 | "prettier": "prettier . --write", 22 | "type-check": "tsc --noEmit" 23 | }, 24 | "dependencies": { 25 | "ws": "8.17.0" 26 | }, 27 | "devDependencies": { 28 | "@sync-your-cookie/tsconfig": "workspace:*", 29 | "@rollup/plugin-sucrase": "^5.0.2", 30 | "@types/ws": "^8.5.10", 31 | "esm": "^3.2.25", 32 | "rollup": "^4.17.2", 33 | "ts-node": "^10.9.2" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/hmr/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import sucrase from '@rollup/plugin-sucrase'; 2 | 3 | const plugins = [ 4 | sucrase({ 5 | exclude: ['node_modules/**'], 6 | transforms: ['typescript'], 7 | }), 8 | ]; 9 | 10 | /** 11 | * @type {import("rollup").RollupOptions[]} 12 | */ 13 | export default [ 14 | { 15 | plugins, 16 | input: 'lib/injections/reload.ts', 17 | output: { 18 | format: 'iife', 19 | file: 'build/injections/reload.js', 20 | }, 21 | }, 22 | { 23 | plugins, 24 | input: 'lib/injections/refresh.ts', 25 | output: { 26 | format: 'iife', 27 | file: 'build/injections/refresh.js', 28 | }, 29 | }, 30 | ]; 31 | -------------------------------------------------------------------------------- /packages/hmr/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "exclude": ["lib/injections/**/*"], 7 | "include": ["lib", "index.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/hmr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["lib", "index.ts", "rollup.config.mjs"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/protobuf/README.md: -------------------------------------------------------------------------------- 1 | # Shared Package 2 | 3 | This package contains code shared with other packages. 4 | To use the code in the package, you need to add the following to the package.json file. 5 | 6 | ```json 7 | { 8 | "dependencies": { 9 | "@sync-your-cookie/shared": "workspace:*" 10 | } 11 | } 12 | ``` 13 | 14 | After building this package, real-time cache busting does not occur in the code of other packages that reference this package. 15 | You need to rerun it from the root path with `pnpm dev`, etc. (This will be improved in the future.) 16 | 17 | If the type does not require compilation, there is no problem, but if the implementation requiring compilation is changed, a problem may occur. 18 | 19 | Therefore, it is recommended to extract and use it in each context if it is easier to manage by extracting overlapping or business logic from the code that changes frequently in this package. 20 | -------------------------------------------------------------------------------- /packages/protobuf/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/protobuf'; 2 | 3 | export * from './utils'; 4 | 5 | import pako from 'pako'; 6 | 7 | export { pako }; 8 | -------------------------------------------------------------------------------- /packages/protobuf/lib/protobuf/code.ts: -------------------------------------------------------------------------------- 1 | import pako from 'pako'; 2 | import { compress, decompress } from './../../utils/compress'; 3 | import type { ICookiesMap } from './proto/cookie'; 4 | import { CookiesMap } from './proto/cookie'; 5 | 6 | export const encodeCookiesMap = async ( 7 | cookiesMap: ICookiesMap = {}, 8 | isCompress: boolean = true, 9 | ): Promise<Uint8Array> => { 10 | // verify 只会校验数据的类型是否合法,并不会校验是否缺少或增加了数据项。 11 | const invalid = CookiesMap.verify(cookiesMap); 12 | if (invalid) { 13 | throw Error(invalid); 14 | } 15 | 16 | const message = CookiesMap.create(cookiesMap); 17 | const buffer = CookiesMap.encode(message).finish(); 18 | if (isCompress) { 19 | const compressedBuf = pako.deflate(buffer); 20 | return await compress(compressedBuf); 21 | } 22 | return buffer; 23 | }; 24 | 25 | export const decodeCookiesMap = async (buffer: Uint8Array, isDeCompress: boolean = true) => { 26 | let buf = buffer; 27 | if (isDeCompress) { 28 | buf = await decompress(buf); 29 | buf = pako.inflate(buf); 30 | } 31 | const message = CookiesMap.decode(buf); 32 | return message; 33 | }; 34 | 35 | export type { ICookie, ICookiesMap } from './proto/cookie'; 36 | -------------------------------------------------------------------------------- /packages/protobuf/lib/protobuf/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code'; 2 | -------------------------------------------------------------------------------- /packages/protobuf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/protobuf", 3 | "version": "0.0.1", 4 | "description": "chrome extension protobuf code", 5 | "private": true, 6 | "sideEffects": false, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "main": "./dist/index.js", 11 | "module": "./dist/index.mjs", 12 | "types": "./dist/index.d.ts", 13 | "scripts": { 14 | "clean": "rimraf ./dist && rimraf .turbo", 15 | "build": "tsup index.ts --format esm,cjs --dts --external react,chrome", 16 | "dev": "tsc -w", 17 | "copy:proto": "cp -r ./lib/protobuf/proto ./dist/lib/protobuf", 18 | "lint": "eslint . --ext .ts,.tsx", 19 | "lint:fix": "pnpm lint --fix", 20 | "prettier": "prettier . --write", 21 | "type-check": "tsc --noEmit", 22 | "proto": "pbjs -o ./lib/protobuf/proto/cookie.js -w es6 -t static-module ./proto/*.proto && pbts ./lib/protobuf/proto/cookie.js -o ./lib/protobuf/proto/cookie.d.ts" 23 | }, 24 | "dependencies": { 25 | "pako": "^2.1.0", 26 | "protobufjs": "^7.3.2" 27 | }, 28 | "devDependencies": { 29 | "@sync-your-cookie/tsconfig": "workspace:*", 30 | "@types/pako": "^2.0.3", 31 | "protobufjs-cli": "^1.1.3", 32 | "tsup": "8.0.2", 33 | "tsx": "^4.19.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/protobuf/proto/cookie.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Cookie { 4 | string domain = 1; 5 | string name = 2; 6 | string storeId = 3; 7 | string value = 4; 8 | bool session = 5; 9 | bool hostOnly = 6; 10 | float expirationDate = 7; 11 | string path = 8; 12 | bool httpOnly = 9; 13 | bool secure = 10; 14 | string sameSite = 11; 15 | } 16 | 17 | message DomainCookie { 18 | int64 createTime = 1; 19 | int64 updateTime = 2; 20 | repeated Cookie cookies = 5; 21 | } 22 | 23 | message CookiesMap { 24 | int64 createTime = 1; 25 | int64 updateTime = 2; 26 | map<string, DomainCookie> domainCookieMap = 5; 27 | } 28 | -------------------------------------------------------------------------------- /packages/protobuf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "react-jsx", 6 | "checkJs": false, 7 | "allowJs": false, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@lib/*": ["lib/*"] 11 | }, 12 | "types": ["chrome"] 13 | }, 14 | "exclude": ["code-test.ts"], 15 | "include": ["index.ts", "lib"], 16 | } 17 | -------------------------------------------------------------------------------- /packages/protobuf/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | treeshake: true, 5 | format: ['cjs', 'esm'], 6 | dts: true, 7 | external: ['chrome'], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/protobuf/utils/base64.ts: -------------------------------------------------------------------------------- 1 | export function arrayBufferToBase64(arrayBuffer: ArrayBuffer) { 2 | let base64 = ''; 3 | const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 4 | 5 | const bytes = new Uint8Array(arrayBuffer); 6 | const byteLength = bytes.byteLength; 7 | const byteRemainder = byteLength % 3; 8 | const mainLength = byteLength - byteRemainder; 9 | 10 | let a, b, c, d; 11 | let chunk; 12 | 13 | // Main loop deals with bytes in chunks of 3 14 | for (let i = 0; i < mainLength; i = i + 3) { 15 | // Combine the three bytes into a single integer 16 | chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; 17 | 18 | // Use bitmasks to extract 6-bit segments from the triplet 19 | a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18 20 | b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12 21 | c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6 22 | d = chunk & 63; // 63 = 2^6 - 1 23 | 24 | // Convert the raw binary segments to the appropriate ASCII encoding 25 | base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]; 26 | } 27 | 28 | // Deal with the remaining bytes and padding 29 | if (byteRemainder == 1) { 30 | chunk = bytes[mainLength]; 31 | 32 | a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2 33 | 34 | // Set the 4 least significant bits to zero 35 | b = (chunk & 3) << 4; // 3 = 2^2 - 1 36 | 37 | base64 += encodings[a] + encodings[b] + '=='; 38 | } else if (byteRemainder == 2) { 39 | chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]; 40 | 41 | a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10 42 | b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4 43 | 44 | // Set the 2 least significant bits to zero 45 | c = (chunk & 15) << 2; // 15 = 2^4 - 1 46 | 47 | base64 += encodings[a] + encodings[b] + encodings[c] + '='; 48 | } 49 | 50 | return base64; 51 | } 52 | 53 | export function base64ToArrayBuffer(base64: string) { 54 | const binaryString = atob(base64); 55 | const bytes = new Uint8Array(binaryString.length); 56 | for (let i = 0; i < binaryString.length; i++) { 57 | bytes[i] = binaryString.charCodeAt(i); 58 | } 59 | return bytes; 60 | } 61 | -------------------------------------------------------------------------------- /packages/protobuf/utils/compress.ts: -------------------------------------------------------------------------------- 1 | async function concatUint8Arrays(uint8arrays: ArrayBuffer[]) { 2 | const blob = new Blob(uint8arrays); 3 | const buffer = await blob.arrayBuffer(); 4 | return new Uint8Array(buffer); 5 | } 6 | 7 | /** 8 | * Compress a string into a Uint8Array. 9 | * @param byteArray 10 | * @param method 11 | * @returns Promise<ArrayBuffer> 12 | */ 13 | export const compress = async (byteArray: Uint8Array, method: CompressionFormat = 'gzip'): Promise<Uint8Array> => { 14 | const stream = new Blob([byteArray]).stream(); 15 | // const byteArray: Uint8Array = new TextEncoder().encode(string); 16 | const compressedStream = stream.pipeThrough(new CompressionStream(method)) as unknown as ArrayBuffer[]; 17 | const chunks: ArrayBuffer[] = []; 18 | for await (const chunk of compressedStream) { 19 | chunks.push(chunk); 20 | } 21 | return await concatUint8Arrays(chunks); 22 | }; 23 | 24 | /** 25 | * Decompress bytes into a Uint8Array. 26 | * 27 | * @param {Uint8Array} compressedBytes 28 | * @returns {Promise<Uint8Array>} 29 | */ 30 | export async function decompress(compressedBytes: Uint8Array) { 31 | // Convert the bytes to a stream. 32 | const stream = new Blob([compressedBytes]).stream(); 33 | 34 | // Create a decompressed stream. 35 | const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip')) as unknown as ArrayBuffer[]; 36 | 37 | // Read all the bytes from this stream. 38 | const chunks = []; 39 | for await (const chunk of decompressedStream) { 40 | chunks.push(chunk); 41 | } 42 | const stringBytes = await concatUint8Arrays(chunks); 43 | return stringBytes; 44 | } 45 | -------------------------------------------------------------------------------- /packages/protobuf/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base64'; 2 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared Package 2 | 3 | This package contains code shared with other packages. 4 | To use the code in the package, you need to add the following to the package.json file. 5 | 6 | ```json 7 | { 8 | "dependencies": { 9 | "@sync-your-cookie/shared": "workspace:*" 10 | } 11 | } 12 | ``` 13 | 14 | After building this package, real-time cache busting does not occur in the code of other packages that reference this package. 15 | You need to rerun it from the root path with `pnpm dev`, etc. (This will be improved in the future.) 16 | 17 | If the type does not require compilation, there is no problem, but if the implementation requiring compilation is changed, a problem may occur. 18 | 19 | Therefore, it is recommended to extract and use it in each context if it is easier to manage by extracting overlapping or business logic from the code that changes frequently in this package. 20 | -------------------------------------------------------------------------------- /packages/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/cloudflare'; 2 | 3 | export * from './lib/cookie'; 4 | export * from './lib/hoc'; 5 | export * from './lib/hooks'; 6 | export * from './lib/Providers'; 7 | 8 | export * from './lib/message'; 9 | export * from './lib/utils'; 10 | 11 | -------------------------------------------------------------------------------- /packages/shared/lib/Providers/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useStorageSuspense } from '@lib/hooks/useStorageSuspense'; 2 | import { themeStorage } from '@sync-your-cookie/storage/lib/themeStorage'; 3 | import { createContext, useEffect } from 'react'; 4 | 5 | type Theme = 'dark' | 'light' | 'system'; 6 | 7 | type ThemeProviderProps = { 8 | children: React.ReactNode; 9 | }; 10 | 11 | type ThemeProviderState = { 12 | theme: Theme; 13 | setTheme: (theme: Theme) => void; 14 | }; 15 | 16 | const initialState: ThemeProviderState = { 17 | theme: 'system', 18 | setTheme: () => null, 19 | }; 20 | 21 | export const ThemeProviderContext = createContext<ThemeProviderState>(initialState); 22 | 23 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 24 | const theme = useStorageSuspense(themeStorage); 25 | 26 | useEffect(() => { 27 | const root = window.document.documentElement; 28 | 29 | root.classList.remove('light', 'dark'); 30 | 31 | if (theme === 'system') { 32 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 33 | root.classList.add(systemTheme); 34 | return; 35 | } 36 | 37 | root.classList.add(theme); 38 | }, [theme]); 39 | 40 | const value = { 41 | theme, 42 | setTheme: (theme: Theme) => { 43 | // localStorage.setItem(storageKey, theme); 44 | themeStorage.set(theme); 45 | // setTheme(theme); 46 | }, 47 | }; 48 | 49 | return ( 50 | <ThemeProviderContext.Provider {...props} value={value}> 51 | {children} 52 | </ThemeProviderContext.Provider> 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /packages/shared/lib/Providers/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTheme'; 2 | -------------------------------------------------------------------------------- /packages/shared/lib/Providers/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeProviderContext } from '../'; 2 | 3 | import { useContext, useEffect } from 'react'; 4 | 5 | export const useTheme = () => { 6 | const context = useContext(ThemeProviderContext); 7 | useEffect(() => { 8 | const handler = (event: MediaQueryListEvent) => { 9 | if (event.matches) { 10 | context.setTheme('dark'); 11 | } else { 12 | context.setTheme('light'); 13 | } 14 | }; 15 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handler); 16 | return () => { 17 | window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', handler); 18 | }; 19 | }, []); 20 | 21 | if (context === undefined) throw new Error('useTheme must be used within a ThemeProvider'); 22 | 23 | return context; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/shared/lib/Providers/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | export * from './ThemeProvider'; 3 | 4 | -------------------------------------------------------------------------------- /packages/shared/lib/cloudflare/api.ts: -------------------------------------------------------------------------------- 1 | import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; 2 | 3 | export interface WriteResponse { 4 | success: boolean; 5 | errors: { 6 | code: number; 7 | message: string; 8 | }[]; 9 | } 10 | 11 | /** 12 | * 13 | * @param value specify the value to write 14 | * @param accountId cloudflare account id 15 | * @param namespaceId cloudflare namespace id 16 | * @param token api token 17 | * @returns promise<res> 18 | */ 19 | export const writeCloudflareKV = async (value: string, accountId: string, namespaceId: string, token: string) => { 20 | const storageKey = settingsStorage.getSnapshot()?.storageKey; 21 | const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${storageKey}`; 22 | // const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/bulk`; 23 | // const payload = [ 24 | // { 25 | // key: DEFAULT_KEY, 26 | // metadata: JSON.stringify({ 27 | // someMetadataKey: value, 28 | // }), 29 | // value: value, 30 | // }, 31 | // ]; 32 | const options = { 33 | method: 'PUT', 34 | headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, 35 | body: value, 36 | }; 37 | return fetch(url, options).then(res => res.json()); 38 | }; 39 | 40 | /** 41 | * 42 | * @param accountId cloudflare account id 43 | * @param namespaceId cloudflare namespace id 44 | * @param token api token 45 | * @returns Promise<res> 46 | */ 47 | export const readCloudflareKV = async (accountId: string, namespaceId: string, token: string) => { 48 | const storageKey = settingsStorage.getSnapshot()?.storageKey; 49 | const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${storageKey}`; 50 | const options = { 51 | method: 'GET', 52 | headers: { 53 | Authorization: `Bearer ${token}`, 54 | 'Content-Type': 'application/json', 55 | }, 56 | }; 57 | return fetch(url, options).then(async res => { 58 | if (res.status === 404) { 59 | return ''; 60 | } 61 | if (res.status === 200) { 62 | const text = await res.text(); 63 | return text.trim(); 64 | } else { 65 | return Promise.reject(await res.json()); 66 | } 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/shared/lib/cloudflare/enum.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorCode { 2 | NotFoundRoute = 7003, 3 | AuthenicationError = 10000, 4 | NamespaceIdError = 10011, 5 | } 6 | -------------------------------------------------------------------------------- /packages/shared/lib/cloudflare/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | 3 | export * from './enum'; 4 | -------------------------------------------------------------------------------- /packages/shared/lib/cookie/index.ts: -------------------------------------------------------------------------------- 1 | export * from './withCloudflare'; 2 | export * from './withStorage'; 3 | 4 | -------------------------------------------------------------------------------- /packages/shared/lib/hoc/index.ts: -------------------------------------------------------------------------------- 1 | import { withSuspense } from './withSuspense'; 2 | import { withErrorBoundary } from './withErrorBoundary'; 3 | 4 | export { withSuspense, withErrorBoundary }; 5 | -------------------------------------------------------------------------------- /packages/shared/lib/hoc/withErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentType, ErrorInfo, ReactElement } from 'react'; 2 | import { Component } from 'react'; 3 | 4 | class ErrorBoundary extends Component< 5 | { 6 | children: ReactElement; 7 | fallback: ReactElement; 8 | }, 9 | { 10 | hasError: boolean; 11 | } 12 | > { 13 | state = { hasError: false }; 14 | 15 | static getDerivedStateFromError() { 16 | return { hasError: true }; 17 | } 18 | 19 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 20 | console.error(error, errorInfo); 21 | } 22 | 23 | render() { 24 | if (this.state.hasError) { 25 | return this.props.fallback; 26 | } 27 | 28 | return this.props.children; 29 | } 30 | } 31 | 32 | export function withErrorBoundary<T extends Record<string, unknown>>( 33 | Component: ComponentType<T>, 34 | ErrorComponent: ReactElement, 35 | ) { 36 | return function WithErrorBoundary(props: T) { 37 | return ( 38 | <ErrorBoundary fallback={ErrorComponent}> 39 | <Component {...props} /> 40 | </ErrorBoundary> 41 | ); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/shared/lib/hoc/withSuspense.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType, ReactElement, Suspense } from 'react'; 2 | 3 | export function withSuspense<T extends Record<string, unknown>>( 4 | Component: ComponentType<T>, 5 | SuspenseComponent: ReactElement, 6 | ) { 7 | return function WithSuspense(props: T) { 8 | return ( 9 | <Suspense fallback={SuspenseComponent}> 10 | <Component {...props} /> 11 | </Suspense> 12 | ); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/shared/lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | import { catchHandler, useCookieAction } from './useCookieAction'; 2 | import { useStorage } from './useStorage'; 3 | import { useStorageSuspense } from './useStorageSuspense'; 4 | export { catchHandler, useCookieAction, useStorage, useStorageSuspense }; 5 | -------------------------------------------------------------------------------- /packages/shared/lib/hooks/useCookieAction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MessageErrorCode, 3 | pullCookieUsingMessage, 4 | pushCookieUsingMessage, 5 | removeCookieUsingMessage, 6 | } from '@lib/message'; 7 | import { domainConfigStorage } from '@sync-your-cookie/storage/lib/domainConfigStorage'; 8 | import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; 9 | 10 | import { toast as Toast } from 'sonner'; 11 | import { useStorageSuspense } from './index'; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | export const catchHandler = (err: any, scene: 'push' | 'pull' | 'remove' | 'delete' | 'edit', toast: typeof Toast) => { 15 | const defaultMsg = `${scene} fail`; 16 | if (err?.code === MessageErrorCode.AccountCheck || err?.code === MessageErrorCode.CloudflareNotFoundRoute) { 17 | toast.error(err?.msg || err?.result?.message || defaultMsg, { 18 | action: { 19 | label: 'go to settings', 20 | onClick: () => { 21 | chrome.runtime.openOptionsPage(); 22 | }, 23 | }, 24 | }); 25 | } else { 26 | toast.error(err?.msg || defaultMsg); 27 | } 28 | console.log('err', err); 29 | }; 30 | 31 | export const useCookieAction = (host: string, toast: typeof Toast) => { 32 | const domainStatus = useStorageSuspense(domainStatusStorage); 33 | const domainConfig = useStorageSuspense(domainConfigStorage); 34 | 35 | const handlePush = async (selectedHost = host, sourceUrl?: string, favIconUrl?: string) => { 36 | return pushCookieUsingMessage({ 37 | host: selectedHost, 38 | sourceUrl, 39 | favIconUrl, 40 | }) 41 | .then(res => { 42 | if (res.isOk) { 43 | toast.success('Pushed success'); 44 | } else { 45 | toast.error(res.msg || 'Pushed fail'); 46 | } 47 | console.log('res', res); 48 | }) 49 | .catch(err => { 50 | catchHandler(err, 'push', toast); 51 | }); 52 | }; 53 | 54 | const handlePull = async (activeTabUrl: string, selectedDomain = host, reload = true) => { 55 | return pullCookieUsingMessage({ 56 | activeTabUrl: activeTabUrl, 57 | domain: selectedDomain, 58 | reload, 59 | }) 60 | .then(res => { 61 | console.log('res', res); 62 | if (res.isOk) { 63 | toast.success('Pull success'); 64 | } else { 65 | toast.error(res.msg || 'Pull fail'); 66 | } 67 | }) 68 | .catch(err => { 69 | catchHandler(err, 'pull', toast); 70 | }); 71 | }; 72 | 73 | const handleRemove = async (selectedDomain = host) => { 74 | return removeCookieUsingMessage({ 75 | domain: selectedDomain, 76 | }) 77 | .then(async res => { 78 | console.log('res', res); 79 | if (res.isOk) { 80 | toast.success(res.msg || 'success'); 81 | await domainConfigStorage.removeItem(host); 82 | } else { 83 | toast.error(res.msg || 'Removed fail'); 84 | } 85 | console.log('res', res); 86 | }) 87 | .catch(err => { 88 | catchHandler(err, 'remove', toast); 89 | }); 90 | }; 91 | 92 | return { 93 | // domainConfig: domainConfig as typeof domainConfig, 94 | pulling: domainStatus.pulling, 95 | pushing: domainStatus.pushing, 96 | domainItemConfig: domainConfig.domainMap[host] || {}, 97 | domainItemStatus: domainStatus.domainMap[host] || {}, 98 | getDomainItemConfig: (selectedDomain: string) => { 99 | return domainConfig.domainMap[selectedDomain] || {}; 100 | }, 101 | getDomainItemStatus: (selectedDomain: string) => { 102 | return domainStatus.domainMap[selectedDomain] || {}; 103 | }, 104 | toggleAutoPullState: domainConfigStorage.toggleAutoPullState, 105 | toggleAutoPushState: domainConfigStorage.toggleAutoPushState, 106 | togglePullingState: domainStatusStorage.togglePullingState, 107 | togglePushingState: domainStatusStorage.togglePushingState, 108 | handlePush, 109 | handlePull, 110 | handleRemove, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /packages/shared/lib/hooks/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from 'react'; 2 | import { BaseStorage } from '@sync-your-cookie/storage'; 3 | 4 | export function useStorage< 5 | Storage extends BaseStorage<Data>, 6 | Data = Storage extends BaseStorage<infer Data> ? Data : unknown, 7 | >(storage: Storage) { 8 | const _data = useSyncExternalStore<Data | null>(storage.subscribe, storage.getSnapshot); 9 | 10 | // eslint-disable-next-line 11 | // @ts-ignore 12 | if (!storageMap.has(storage)) { 13 | // eslint-disable-next-line 14 | // @ts-ignore 15 | storageMap.set(storage, wrapPromise(storage.get())); 16 | } 17 | if (_data !== null) { 18 | // eslint-disable-next-line 19 | // @ts-ignore 20 | storageMap.set(storage, { read: () => _data }); 21 | } 22 | // eslint-disable-next-line 23 | // @ts-ignore 24 | return _data ?? (storageMap.get(storage)!.read() as Data); 25 | } 26 | -------------------------------------------------------------------------------- /packages/shared/lib/hooks/useStorageSuspense.tsx: -------------------------------------------------------------------------------- 1 | import { BaseStorage } from '@sync-your-cookie/storage'; 2 | import { useSyncExternalStore } from 'react'; 3 | 4 | type WrappedPromise = ReturnType<typeof wrapPromise>; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | const storageMap: Map<BaseStorage<any>, WrappedPromise> = new Map(); 7 | 8 | export function useStorageSuspense< 9 | Storage extends BaseStorage<Data>, 10 | Data = Storage extends BaseStorage<infer Data> ? Data : unknown, 11 | >(storage: Storage) { 12 | const _data = useSyncExternalStore<Data | null>(storage.subscribe, storage.getSnapshot); 13 | if (!storageMap.has(storage)) { 14 | storageMap.set(storage, wrapPromise(storage.get())); 15 | } 16 | if (_data !== null) { 17 | storageMap.set(storage, { read: () => _data }); 18 | } 19 | 20 | return _data ?? (storageMap.get(storage)!.read() as Data); 21 | } 22 | 23 | function wrapPromise<R>(promise: Promise<R>) { 24 | let status = 'pending'; 25 | let result: R; 26 | const suspender = promise.then( 27 | r => { 28 | status = 'success'; 29 | result = r; 30 | }, 31 | e => { 32 | status = 'error'; 33 | result = e; 34 | }, 35 | ); 36 | 37 | return { 38 | read() { 39 | switch (status) { 40 | case 'pending': 41 | throw suspender; 42 | case 'error': 43 | throw result; 44 | default: 45 | return result; 46 | } 47 | }, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /packages/shared/lib/message/index.ts: -------------------------------------------------------------------------------- 1 | import { ICookie } from '@sync-your-cookie/protobuf'; 2 | 3 | export type { ICookie }; 4 | export enum MessageType { 5 | PushCookie = 'PushCookie', 6 | PullCookie = 'PullCookie', 7 | RemoveCookie = 'RemoveCookie', 8 | RemoveCookieItem = 'RemoveCookieItem', 9 | EditCookieItem = 'EditCookieItem', 10 | } 11 | 12 | export enum MessageErrorCode { 13 | AccountCheck = 'AccountCheck', 14 | CloudflareNotFoundRoute = 'CloudflareNotFoundRoute', 15 | } 16 | 17 | export type PushCookieMessagePayload = { 18 | host: string; 19 | sourceUrl?: string; 20 | favIconUrl?: string; 21 | }; 22 | 23 | export type RemoveCookieMessagePayload = { 24 | domain: string; 25 | }; 26 | 27 | export type RemoveCookieItemMessagePayload = { 28 | domain: string; 29 | id: string; 30 | }; 31 | 32 | export type PullCookieMessagePayload = { 33 | domain: string; 34 | activeTabUrl: string; 35 | reload: boolean; 36 | }; 37 | 38 | export type EditCookieItemMessagePayload = { 39 | domain: string; 40 | oldItem: ICookie; 41 | newItem: ICookie; 42 | }; 43 | 44 | export type MessageMap = { 45 | [MessageType.PushCookie]: { 46 | type: MessageType.PushCookie; 47 | payload: PushCookieMessagePayload; 48 | }; 49 | [MessageType.RemoveCookie]: { 50 | type: MessageType.RemoveCookie; 51 | payload: RemoveCookieMessagePayload; 52 | }; 53 | [MessageType.PullCookie]: { 54 | type: MessageType.PullCookie; 55 | payload: PullCookieMessagePayload; 56 | }; 57 | [MessageType.RemoveCookieItem]: { 58 | type: MessageType.RemoveCookieItem; 59 | payload: RemoveCookieItemMessagePayload; 60 | }; 61 | [MessageType.EditCookieItem]: { 62 | type: MessageType.EditCookieItem; 63 | payload: EditCookieItemMessagePayload; 64 | }; 65 | }; 66 | 67 | // export type Message<T extends MessageType = MessageType> = { 68 | // type: T; 69 | // payload: MessagePayloadMap[T]; 70 | // }; 71 | 72 | export type Message<T extends MessageType = MessageType> = MessageMap[T]; 73 | 74 | export type SendResponse = { 75 | isOk: boolean; 76 | msg: string; 77 | result?: unknown; 78 | code?: MessageErrorCode; 79 | }; 80 | 81 | export function sendMessage<T extends MessageType>(message: Message<T>, isTab = false) { 82 | if (isTab) { 83 | return new Promise<SendResponse>((resolve, reject) => { 84 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 85 | if (tabs.length === 0) { 86 | reject({ isOk: false, msg: 'No active tab found' } as SendResponse); 87 | return; 88 | } 89 | chrome.tabs.sendMessage(tabs[0].id!, message, function (result) { 90 | console.log('isTab', isTab, 'result->', result); 91 | if (result?.isOk) { 92 | resolve(result); 93 | } else { 94 | reject(result as SendResponse); 95 | } 96 | }); 97 | }); 98 | }); 99 | } 100 | return new Promise<SendResponse>((resolve, reject) => { 101 | chrome.runtime.sendMessage(message, function (result: SendResponse) { 102 | if (result?.isOk) { 103 | resolve(result); 104 | } else { 105 | reject(result as SendResponse); 106 | } 107 | }); 108 | }); 109 | } 110 | 111 | export function pushCookieUsingMessage(payload: PushCookieMessagePayload) { 112 | return sendMessage<MessageType.PushCookie>({ 113 | payload, 114 | type: MessageType.PushCookie, 115 | }); 116 | } 117 | 118 | export function removeCookieUsingMessage(payload: RemoveCookieMessagePayload) { 119 | return sendMessage<MessageType.RemoveCookie>({ 120 | payload, 121 | type: MessageType.RemoveCookie, 122 | }); 123 | } 124 | 125 | export function pullCookieUsingMessage(payload: PullCookieMessagePayload) { 126 | return sendMessage<MessageType.PullCookie>({ 127 | payload, 128 | type: MessageType.PullCookie, 129 | }); 130 | } 131 | 132 | export function removeCookieItemUsingMessage(payload: RemoveCookieItemMessagePayload) { 133 | const sendType = MessageType.RemoveCookieItem; 134 | return sendMessage<typeof sendType>({ 135 | payload, 136 | type: sendType, 137 | }); 138 | } 139 | 140 | export function editCookieItemUsingMessage(payload: EditCookieItemMessagePayload) { 141 | const sendType = MessageType.EditCookieItem; 142 | return sendMessage<typeof sendType>({ 143 | payload, 144 | type: sendType, 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /packages/shared/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, WriteResponse } from '@lib/cloudflare'; 2 | import { MessageErrorCode, SendResponse } from '@lib/message'; 3 | 4 | export function debounce<T = unknown>(func: (...args: T[]) => void, timeout = 300) { 5 | let timer: number | null = null; 6 | return (...args: T[]) => { 7 | timer && clearTimeout(timer); 8 | timer = setTimeout(() => { 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore 11 | func.apply(this, args); 12 | }, timeout); 13 | }; 14 | } 15 | 16 | export function checkCloudflareResponse( 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | res: WriteResponse | Error | any, 19 | scene: 'push' | 'pull' | 'remove' | 'delete' | 'edit', 20 | callback: (response?: SendResponse) => void, 21 | ) { 22 | if ((res as WriteResponse)?.success) { 23 | callback({ isOk: true, msg: `${scene} success` }); 24 | } else { 25 | const cloudFlareErrors = [ErrorCode.NotFoundRoute, ErrorCode.NamespaceIdError, ErrorCode.AuthenicationError]; 26 | const isAccountError = res?.errors?.length && cloudFlareErrors.includes(res.errors[0].code); 27 | if (isAccountError) { 28 | callback({ 29 | isOk: false, 30 | msg: 31 | res.errors[0].code === ErrorCode.NamespaceIdError 32 | ? 'cloudflare namespace Id info is incorrect.' 33 | : 'cloudflare account info is incorrect.', 34 | code: MessageErrorCode.CloudflareNotFoundRoute, 35 | result: res, 36 | }); 37 | } else { 38 | const defaultErrMsg = 39 | res?.message?.toLowerCase().includes?.(scene) || (res?.code && res?.message) 40 | ? res?.message 41 | : `${scene} fail, please try again.`; 42 | callback({ isOk: false, code: res?.code, msg: defaultErrMsg, result: res }); 43 | } 44 | } 45 | } 46 | function addProtocol(uri: string) { 47 | return uri.startsWith('http') ? uri : `http://${uri}`; 48 | } 49 | 50 | export async function extractDomainAndPort(url: string, isRemoveWWW = true): Promise<[string, string]> { 51 | let urlObj: URL; 52 | try { 53 | const maybeValidUrl = addProtocol(url); 54 | urlObj = new URL(maybeValidUrl); 55 | } catch (error) { 56 | return [url, '']; 57 | } 58 | let domain = urlObj.hostname; 59 | const port = urlObj.port; 60 | domain = domain.replace('http://', '').replace('https://', ''); 61 | if (isRemoveWWW) { 62 | domain = domain.replace('www.', ''); 63 | } 64 | // match ip address 65 | if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(domain)) { 66 | return [domain, port]; 67 | } 68 | if (domain.split('.').length <= 2) { 69 | return [domain, port]; 70 | } 71 | return new Promise(resolve => { 72 | try { 73 | chrome.cookies.getAll( 74 | { 75 | url, 76 | }, 77 | async cookies => { 78 | console.log('cookies', cookies); 79 | if (cookies) { 80 | const domain = cookies[0].domain; 81 | if (domain.startsWith('.')) { 82 | resolve([domain.slice(1), port]); 83 | } else { 84 | resolve([domain, port]); 85 | } 86 | } else { 87 | const match = domain.match(/([^.]+\.[^.]+)$/); 88 | resolve([match ? match[1] : '', port]); 89 | } 90 | }, 91 | ); 92 | } catch (error) { 93 | console.error('error', error); 94 | const match = domain.match(/([^.]+\.[^.]+)$/); 95 | resolve([match ? match[1] : '', port]); 96 | } 97 | }); 98 | } 99 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/shared", 3 | "version": "0.0.1", 4 | "description": "chrome extension shared code", 5 | "private": true, 6 | "sideEffects": false, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "main": "./dist/index.js", 11 | "module": "./dist/index.mjs", 12 | "types": "./dist/index.d.ts", 13 | "scripts": { 14 | "clean": "rimraf ./dist && rimraf .turbo", 15 | "build": "tsup index.ts --format esm,cjs --dts --external react,chrome", 16 | "dev": "tsc -w", 17 | "lint": "eslint . --ext .ts,.tsx", 18 | "lint:fix": "pnpm lint --fix", 19 | "prettier": "prettier . --write", 20 | "type-check": "tsc --noEmit", 21 | "proto": "pbjs -o ./lib/protobuf/proto/cookie.js -w es6 -t static-module ./lib/protobuf/proto/*.proto && pbts ./lib/protobuf/proto/cookie.js -o ./lib/protobuf/proto/cookie.d.ts" 22 | }, 23 | "dependencies": { 24 | "pako": "^2.1.0", 25 | "protobufjs": "^7.3.2" 26 | }, 27 | "devDependencies": { 28 | "@sync-your-cookie/storage": "workspace:*", 29 | "@sync-your-cookie/protobuf": "workspace:*", 30 | "@sync-your-cookie/tsconfig": "workspace:*", 31 | "@types/pako": "^2.0.3", 32 | "tsup": "8.0.2", 33 | "sonner": "^1.5.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "react-jsx", 6 | "checkJs": false, 7 | "allowJs": false, 8 | "baseUrl": ".", 9 | "declarationMap": true, 10 | "paths": { 11 | "@lib/*": ["lib/*"] 12 | }, 13 | "types": ["chrome"] 14 | }, 15 | "include": ["index.ts", "lib"], 16 | } 17 | -------------------------------------------------------------------------------- /packages/shared/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | treeshake: true, 5 | splitting: false, 6 | format: ['cjs', 'esm'], 7 | dts: true, 8 | external: ['chrome'], 9 | }); 10 | -------------------------------------------------------------------------------- /packages/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/storage/lib/cloudflareStorage.ts: -------------------------------------------------------------------------------- 1 | import { BaseStorage, createStorage, StorageType } from './base'; 2 | 3 | export interface AccountInfo { 4 | accountId?: string; 5 | namespaceId?: string; 6 | token?: string; 7 | } 8 | const key = 'cloudflare-account-storage-key'; 9 | const cacheStorageMap = new Map(); 10 | 11 | const initStorage = (): BaseStorage<AccountInfo> => { 12 | if (cacheStorageMap.has(key)) { 13 | return cacheStorageMap.get(key); 14 | } 15 | const storage = createStorage<AccountInfo>( 16 | key, 17 | {}, 18 | { 19 | storageType: StorageType.Sync, 20 | liveUpdate: true, 21 | }, 22 | ); 23 | cacheStorageMap.set(key, storage); 24 | return storage; 25 | }; 26 | 27 | const storage = initStorage(); 28 | 29 | type CloudflareStorage = BaseStorage<AccountInfo> & { 30 | update: (updateInfo: AccountInfo) => Promise<void>; 31 | }; 32 | 33 | export const cloudflareStorage: CloudflareStorage = { 34 | ...storage, 35 | update: async (updateInfo: AccountInfo) => { 36 | await storage.set(currentInfo => { 37 | return { ...currentInfo, ...updateInfo }; 38 | }); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/storage/lib/cookieStorage.ts: -------------------------------------------------------------------------------- 1 | import { ICookie, ICookiesMap } from '@sync-your-cookie/protobuf'; 2 | import { BaseStorage, createStorage, StorageType } from './base'; 3 | 4 | export interface Cookie extends ICookiesMap {} 5 | 6 | const cacheStorageMap = new Map(); 7 | const key = 'cookie-storage-key'; 8 | 9 | const initStorage = (): BaseStorage<Cookie> => { 10 | if (cacheStorageMap.has(key)) { 11 | return cacheStorageMap.get(key); 12 | } 13 | const storage: BaseStorage<Cookie> = createStorage<Cookie>( 14 | key, 15 | {}, 16 | { 17 | storageType: StorageType.Local, 18 | liveUpdate: true, 19 | }, 20 | ); 21 | cacheStorageMap.set(key, storage); 22 | return storage; 23 | }; 24 | 25 | const storage = initStorage(); 26 | 27 | export const cookieStorage = { 28 | ...storage, 29 | reset: async () => { 30 | await storage.set(() => { 31 | return {}; 32 | }); 33 | }, 34 | updateItem: async (domain: string, updateCookies: ICookie[]) => { 35 | let newVal: Cookie = {}; 36 | await storage.set(currentInfo => { 37 | const domainCookieMap = currentInfo.domainCookieMap || {}; 38 | currentInfo.createTime = currentInfo.createTime || Date.now(); 39 | currentInfo.updateTime = Date.now(); 40 | domainCookieMap[domain] = { 41 | ...domainCookieMap[domain], 42 | cookies: updateCookies, 43 | }; 44 | newVal = { ...currentInfo, domainCookieMap }; 45 | return newVal; 46 | }); 47 | return newVal; 48 | }, 49 | update: async (updateInfo: Cookie, isInit = false) => { 50 | let newVal: Cookie = {}; 51 | await storage.set(currentInfo => { 52 | newVal = isInit ? updateInfo : { ...currentInfo, ...updateInfo }; 53 | return newVal; 54 | }); 55 | return newVal; 56 | }, 57 | removeItem: async (domain: string) => { 58 | let newVal: Cookie = {}; 59 | await storage.set(currentInfo => { 60 | const domainCookieMap = currentInfo.domainCookieMap || {}; 61 | delete domainCookieMap[domain]; 62 | newVal = { ...currentInfo, domainCookieMap }; 63 | return newVal; 64 | }); 65 | return newVal; 66 | }, 67 | 68 | removeDomainItem: async (domain: string, name: string) => { 69 | let newVal: Cookie = {}; 70 | await storage.set(currentInfo => { 71 | const domainCookieMap = currentInfo.domainCookieMap || {}; 72 | const domainCookies = domainCookieMap[domain] || {}; 73 | const cookies = domainCookies.cookies || []; 74 | const newCookies = cookies.filter(cookie => cookie.name !== name); 75 | domainCookieMap[domain] = { 76 | ...domainCookies, 77 | cookies: newCookies, 78 | }; 79 | newVal = { ...currentInfo, domainCookieMap }; 80 | return newVal; 81 | }); 82 | return newVal; 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /packages/storage/lib/domainConfigStorage.ts: -------------------------------------------------------------------------------- 1 | import { BaseStorage, createStorage, StorageType } from './base'; 2 | 3 | type DomainItemConfig = { 4 | autoPull?: boolean; 5 | autoPush?: boolean; 6 | favIconUrl?: string; 7 | sourceUrl?: string; 8 | }; 9 | 10 | interface DomainConfig { 11 | domainMap: { 12 | [host: string]: DomainItemConfig; 13 | }; 14 | } 15 | const key = 'domainConfig-storage-key'; 16 | 17 | const cacheStorageMap = new Map(); 18 | const initStorage = (): BaseStorage<DomainConfig> => { 19 | if (cacheStorageMap.has(key)) { 20 | return cacheStorageMap.get(key); 21 | } 22 | const storage: BaseStorage<DomainConfig> = createStorage<DomainConfig>( 23 | key, 24 | { 25 | domainMap: {}, 26 | }, 27 | { 28 | storageType: StorageType.Local, 29 | liveUpdate: true, 30 | // onLoad: onLoad, 31 | }, 32 | ); 33 | cacheStorageMap.set(key, storage); 34 | return storage; 35 | }; 36 | 37 | const storage = initStorage(); 38 | 39 | export const domainConfigStorage = { 40 | ...storage, 41 | updateItem: async (host: string, updateConf: DomainItemConfig) => { 42 | return await storage.set(currentInfo => { 43 | const domainMap = currentInfo?.domainMap || {}; 44 | domainMap[host] = { 45 | ...domainMap[host], 46 | ...updateConf, 47 | }; 48 | return { ...(currentInfo || {}), domainMap }; 49 | }); 50 | }, 51 | update: async (updateInfo: Partial<DomainConfig>) => { 52 | return await storage.set(currentInfo => { 53 | return { ...currentInfo, ...updateInfo }; 54 | }); 55 | }, 56 | removeItem: async (domain: string) => { 57 | await storage.set(currentInfo => { 58 | const domainCookieMap = currentInfo.domainMap || {}; 59 | delete domainCookieMap[domain]; 60 | return { ...currentInfo, domainCookieMap }; 61 | }); 62 | }, 63 | 64 | toggleAutoPullState: async (domain: string, checked?: boolean) => { 65 | return await storage.set(currentInfo => { 66 | const domainMap = currentInfo?.domainMap || {}; 67 | domainMap[domain] = { 68 | ...domainMap[domain], 69 | autoPull: checked ?? !domainMap[domain]?.autoPull, 70 | }; 71 | return { ...(currentInfo || {}), domainMap }; 72 | }); 73 | }, 74 | 75 | toggleAutoPushState: async (domain: string, checked?: boolean) => { 76 | return await storage.set(currentInfo => { 77 | const domainMap = currentInfo?.domainMap || {}; 78 | domainMap[domain] = { 79 | ...domainMap[domain], 80 | autoPush: checked ?? !domainMap[domain]?.autoPush, 81 | }; 82 | return { ...(currentInfo || {}), domainMap }; 83 | }); 84 | }, 85 | }; 86 | -------------------------------------------------------------------------------- /packages/storage/lib/domainStatusStorage.ts: -------------------------------------------------------------------------------- 1 | import { BaseStorage, createStorage, StorageType } from './base'; 2 | 3 | type DomainItemConfig = { 4 | pulling?: boolean; 5 | pushing?: boolean; 6 | }; 7 | 8 | interface DomainConfig { 9 | pulling: boolean; 10 | pushing: boolean; 11 | domainMap: { 12 | [host: string]: DomainItemConfig; 13 | }; 14 | } 15 | const key = 'domainStatus-storage-key'; 16 | 17 | const cacheStorageMap = new Map(); 18 | const initStorage = (): BaseStorage<DomainConfig> => { 19 | if (cacheStorageMap.has(key)) { 20 | return cacheStorageMap.get(key); 21 | } 22 | const storage: BaseStorage<DomainConfig> = createStorage<DomainConfig>( 23 | key, 24 | { 25 | pulling: false, 26 | pushing: false, 27 | domainMap: {}, 28 | }, 29 | { 30 | storageType: StorageType.Session, 31 | liveUpdate: true, 32 | // onLoad: onLoad, 33 | }, 34 | ); 35 | cacheStorageMap.set(key, storage); 36 | return storage; 37 | }; 38 | 39 | const storage = initStorage(); 40 | 41 | export const domainStatusStorage = { 42 | ...storage, 43 | resetState: async () => { 44 | return await storage.set(currentInfo => { 45 | const domainMap = currentInfo?.domainMap || {}; 46 | for (const domain in domainMap) { 47 | if (domain) { 48 | domainMap[domain] = { 49 | ...domainMap[domain], 50 | pulling: false, 51 | pushing: false, 52 | }; 53 | } else { 54 | delete domainMap[domain]; 55 | } 56 | } 57 | const resetInfo = { 58 | pulling: false, 59 | pushing: false, 60 | domainMap: domainMap, 61 | }; 62 | return resetInfo; 63 | }); 64 | }, 65 | updateItem: async (host: string, updateConf: DomainItemConfig) => { 66 | return await storage.set(currentInfo => { 67 | const domainMap = currentInfo?.domainMap || {}; 68 | domainMap[host] = { 69 | ...domainMap[host], 70 | ...updateConf, 71 | }; 72 | return { ...(currentInfo || {}), domainMap }; 73 | }); 74 | }, 75 | update: async (updateInfo: Partial<DomainConfig>) => { 76 | return await storage.set(currentInfo => { 77 | return { ...currentInfo, ...updateInfo }; 78 | }); 79 | }, 80 | removeItem: async (domain: string) => { 81 | await storage.set(currentInfo => { 82 | const domainCookieMap = currentInfo.domainMap || {}; 83 | delete domainCookieMap[domain]; 84 | return { ...currentInfo, domainCookieMap }; 85 | }); 86 | }, 87 | 88 | togglePullingState: async (domain: string, checked?: boolean) => { 89 | return await storage.set(currentInfo => { 90 | const domainMap = currentInfo?.domainMap || {}; 91 | domainMap[domain] = { 92 | ...domainMap[domain], 93 | pulling: checked ?? !domainMap[domain]?.pulling, 94 | }; 95 | return { ...(currentInfo || {}), domainMap }; 96 | }); 97 | }, 98 | 99 | togglePushingState: async (domain: string, checked?: boolean) => { 100 | return await storage.set(currentInfo => { 101 | const domainMap = currentInfo?.domainMap || {}; 102 | domainMap[domain] = { 103 | ...domainMap[domain], 104 | pushing: checked ?? !domainMap[domain]?.pushing, 105 | }; 106 | return { ...(currentInfo || {}), domainMap }; 107 | }); 108 | }, 109 | }; 110 | -------------------------------------------------------------------------------- /packages/storage/lib/index.ts: -------------------------------------------------------------------------------- 1 | import { SessionAccessLevel, StorageType, createStorage, type BaseStorage } from './base'; 2 | // export * from './cloudflareStorage'; 3 | // export * from './cookieStorage'; 4 | // export * from './domainConfigStorage'; 5 | // export * from './themeStorage'; 6 | 7 | export { BaseStorage, SessionAccessLevel, StorageType, createStorage }; 8 | -------------------------------------------------------------------------------- /packages/storage/lib/settingsStorage.ts: -------------------------------------------------------------------------------- 1 | import { BaseStorage, createStorage, StorageType } from './base'; 2 | 3 | export interface ISettings { 4 | storageKeyList: string[]; 5 | storageKey?: string; 6 | protobufEncoding?: boolean; 7 | } 8 | const key = 'settings-storage-key'; 9 | const cacheStorageMap = new Map(); 10 | export const defaultKey = 'sync-your-cookie'; 11 | 12 | const initStorage = (): BaseStorage<ISettings> => { 13 | if (cacheStorageMap.has(key)) { 14 | return cacheStorageMap.get(key); 15 | } 16 | const storage = createStorage<ISettings>( 17 | key, 18 | { 19 | storageKeyList: [defaultKey], 20 | storageKey: defaultKey, 21 | protobufEncoding: true, 22 | }, 23 | { 24 | storageType: StorageType.Sync, 25 | liveUpdate: true, 26 | }, 27 | ); 28 | cacheStorageMap.set(key, storage); 29 | return storage; 30 | }; 31 | 32 | const storage = initStorage(); 33 | 34 | type TSettingsStorage = BaseStorage<ISettings> & { 35 | update: (updateInfo: Partial<ISettings>) => Promise<void>; 36 | addStorageKey: (key: string) => Promise<void>; 37 | removeStorageKey: (key: string) => Promise<void>; 38 | // getStorageKeyList: () => Promise<string[]>; 39 | }; 40 | 41 | export const settingsStorage: TSettingsStorage = { 42 | ...storage, 43 | update: async (updateInfo: Partial<ISettings>) => { 44 | await storage.set(currentInfo => { 45 | return { ...currentInfo, ...updateInfo }; 46 | }); 47 | }, 48 | 49 | addStorageKey: async (key: string) => { 50 | await storage.set(currentInfo => { 51 | if (currentInfo.storageKeyList.includes(key)) { 52 | return currentInfo; 53 | } 54 | return { 55 | ...currentInfo, 56 | storageKeyList: [...currentInfo.storageKeyList, key], 57 | }; 58 | }); 59 | }, 60 | 61 | removeStorageKey: async (key: string) => { 62 | await storage.set(currentInfo => { 63 | if (!currentInfo.storageKeyList.includes(key)) { 64 | return currentInfo; 65 | } 66 | return { 67 | ...currentInfo, 68 | storageKeyList: currentInfo.storageKeyList.filter(item => item !== key), 69 | }; 70 | }); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /packages/storage/lib/themeStorage.ts: -------------------------------------------------------------------------------- 1 | import { BaseStorage, createStorage, StorageType } from './base'; 2 | 3 | type Theme = 'light' | 'dark' | 'system'; 4 | 5 | type ThemeStorage = BaseStorage<Theme> & { 6 | toggle: () => Promise<void>; 7 | }; 8 | const cacheStorageMap = new Map(); 9 | const key = 'theme-storage-key'; 10 | 11 | const initStorage = (): BaseStorage<Theme> => { 12 | if (cacheStorageMap.has(key)) { 13 | console.log('key', key); 14 | return cacheStorageMap.get(key); 15 | } 16 | const storage = createStorage<Theme>(key, 'light', { 17 | storageType: StorageType.Local, 18 | liveUpdate: true, 19 | }); 20 | cacheStorageMap.set(key, storage); 21 | return storage; 22 | }; 23 | 24 | const storage = initStorage(); 25 | 26 | export const themeStorage: ThemeStorage = { 27 | ...storage, 28 | toggle: async () => { 29 | await storage.set((currentTheme: string) => { 30 | return currentTheme === 'light' ? 'dark' : 'light'; 31 | }); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/storage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/storage", 3 | "version": "0.0.1", 4 | "description": "chrome extension storage", 5 | "private": true, 6 | "sideEffects": false, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "main": "./dist/index.js", 11 | "module": "./dist/index.mjs", 12 | "types": "./dist/index.d.ts", 13 | "scripts": { 14 | "clean": "rimraf ./dist && rimraf .turbo", 15 | "build": "tsup index.ts --format esm,cjs --dts", 16 | "dev": "tsc -w", 17 | "lint": "eslint . --ext .ts,.tsx", 18 | "lint:fix": "pnpm lint --fix", 19 | "prettier": "prettier . --write", 20 | "type-check": "tsc --noEmit" 21 | }, 22 | "dependencies": {}, 23 | "devDependencies": { 24 | "@sync-your-cookie/tsconfig": "workspace:*", 25 | "@sync-your-cookie/protobuf": "workspace:*", 26 | "tsup": "8.0.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "jsx": "react-jsx", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@lib/*": ["lib/*"] 9 | }, 10 | "types": ["chrome"] 11 | }, 12 | "include": ["index.ts", "lib"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/tailwindcss-config", 3 | "version": "1.0.0", 4 | "description": "Tailwind CSS configuration for boilerplate", 5 | "main": "./tailwind.config.js", 6 | "private": true 7 | } 8 | -------------------------------------------------------------------------------- /packages/tailwind-config/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | /** shared theme configuration */ 4 | theme: { 5 | extend: {}, 6 | }, 7 | /** shared plugins configuration */ 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/tsconfig/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Chrome Extension App", 4 | "extends": "./base.json" 5 | } 6 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Base", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "allowJs": true, 7 | "noEmit": true, 8 | "downlevelIteration": true, 9 | "isolatedModules": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "moduleResolution": "node", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "noImplicitReturns": true, 20 | "jsx": "react-jsx", 21 | "lib": [ 22 | "DOM", 23 | "ESNext" 24 | ], 25 | "plugins": [ 26 | { 27 | "transform": "typescript-transform-paths" 28 | }, 29 | ] 30 | } 31 | } 32 | 33 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/tsconfig", 3 | "version": "1.0.0", 4 | "description": "tsconfig for chrome extension", 5 | "private": true, 6 | "scripts": { 7 | "prepare": "ts-patch install -s" 8 | }, 9 | "devDependencies": { 10 | "ts-patch": "^3.2.1", 11 | "typescript-transform-paths": "^3.4.10" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/tsconfig/utils.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Chrome Extension Utils", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "noEmit": false, 7 | "declaration": true, 8 | "module": "CommonJS", 9 | "moduleResolution": "Node", 10 | "declarationMap": true, 11 | "target": "ES6", 12 | "types": ["node"], 13 | "plugins": [ 14 | { 15 | "transform": "typescript-transform-paths" 16 | }, 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "./globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "src/components", 15 | "utils": "@libs/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 224 71.4% 4.1%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 224 71.4% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 224 71.4% 4.1%; 13 | --primary: 262.1 83.3% 57.8%; 14 | --primary-foreground: 210 20% 98%; 15 | --secondary: 220 14.3% 95.9%; 16 | --secondary-foreground: 220.9 39.3% 11%; 17 | --muted: 220 14.3% 95.9%; 18 | --muted-foreground: 220 8.9% 46.1%; 19 | --accent: 220 14.3% 95.9%; 20 | --accent-foreground: 220.9 39.3% 11%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 210 20% 98%; 23 | --border: 220 13% 91%; 24 | --input: 220 13% 91%; 25 | --ring: 262.1 83.3% 57.8%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 224 71.4% 4.1%; 36 | --foreground: 210 20% 98%; 37 | --card: 224 71.4% 4.1%; 38 | --card-foreground: 210 20% 98%; 39 | --popover: 224 71.4% 4.1%; 40 | --popover-foreground: 210 20% 98%; 41 | --primary: 263.4 70% 50.4%; 42 | --primary-foreground: 210 20% 98%; 43 | --secondary: 215 27.9% 16.9%; 44 | --secondary-foreground: 210 20% 98%; 45 | --muted: 215 27.9% 16.9%; 46 | --muted-foreground: 217.9 10.6% 64.9%; 47 | --accent: 215 27.9% 16.9%; 48 | --accent-foreground: 210 20% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 210 20% 98%; 51 | --border: 215 27.9% 16.9%; 52 | --input: 215 27.9% 16.9%; 53 | --ring: 263.4 70% 50.4%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | 63 | @layer base { 64 | * { 65 | @apply border-border; 66 | } 67 | body { 68 | @apply bg-background text-foreground; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/ui", 3 | "version": "0.0.1", 4 | "description": "chrome extension ui", 5 | "private": true, 6 | "sideEffects": false, 7 | "type": "module", 8 | "files": [ 9 | "dist/**", 10 | "dist/**/*.css" 11 | ], 12 | "main": "./dist/index.js", 13 | "module": "./dist/index.js", 14 | "types": "./dist/index.d.ts", 15 | "exports": { 16 | ".": { 17 | "types": "./dist/index.d.ts", 18 | "import": "./dist/index.js", 19 | "require": "./dist/index.js" 20 | }, 21 | "./css": "./globals.css", 22 | "./tailwind.config": "./tailwind.config.js" 23 | }, 24 | "scripts": { 25 | "clean": "rimraf ./dist && rimraf .turbo", 26 | "dev:tsup": "tsup src/index.ts --format esm,cjs --dts --external react,chrome --watch", 27 | "build:tsup": "tsup src/index.ts --format esm,cjs --dts --external react,chrome --watch", 28 | "dev": "tsc -w", 29 | "build": "tsc", 30 | "lint": "eslint . --ext .ts,.tsx", 31 | "lint:fix": "pnpm lint --fix", 32 | "prettier": "prettier . --write", 33 | "type-check": "tsc --noEmit" 34 | }, 35 | "dependencies": { 36 | "@radix-ui/react-alert-dialog": "^1.1.2", 37 | "@radix-ui/react-avatar": "^1.1.1", 38 | "@radix-ui/react-dropdown-menu": "^2.1.2", 39 | "@radix-ui/react-label": "^2.1.0", 40 | "@radix-ui/react-popover": "^1.1.2", 41 | "@radix-ui/react-select": "^2.2.5", 42 | "@radix-ui/react-slot": "^1.1.0", 43 | "@radix-ui/react-switch": "^1.1.1", 44 | "@radix-ui/react-tooltip": "^1.1.3", 45 | "@tanstack/react-table": "^8.20.5", 46 | "class-variance-authority": "^0.7.0", 47 | "clsx": "^2.1.1", 48 | "lucide-react": "^0.394.0", 49 | "next-themes": "^0.3.0", 50 | "sonner": "^1.5.0", 51 | "tailwind-merge": "^2.3.0", 52 | "tailwindcss-animate": "^1.0.7" 53 | }, 54 | "devDependencies": { 55 | "@sync-your-cookie/tailwindcss-config": "workspace:*", 56 | "@sync-your-cookie/tsconfig": "workspace:*", 57 | "tsup": "8.0.2", 58 | "typescript-transform-paths": "^3.4.10" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ui/src/components/DateTable/index.tsx: -------------------------------------------------------------------------------- 1 | import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; 2 | 3 | import type { ColumnDef } from '@tanstack/react-table'; 4 | 5 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; 6 | 7 | interface DataTableProps<TData, TValue> { 8 | columns: ColumnDef<TData, TValue>[]; 9 | data: TData[]; 10 | } 11 | // export * from '@tanstack/react-table'; 12 | export { ColumnDef }; 13 | 14 | export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) { 15 | const table = useReactTable({ 16 | data, 17 | columns, 18 | getCoreRowModel: getCoreRowModel(), 19 | }); 20 | 21 | return ( 22 | <div className="rounded-md border"> 23 | <Table> 24 | <TableHeader> 25 | {table.getHeaderGroups().map(headerGroup => ( 26 | <TableRow key={headerGroup.id}> 27 | {headerGroup.headers.map(header => { 28 | return ( 29 | <TableHead key={header.id}> 30 | {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} 31 | </TableHead> 32 | ); 33 | })} 34 | </TableRow> 35 | ))} 36 | </TableHeader> 37 | <TableBody> 38 | {table.getRowModel().rows?.length ? ( 39 | table.getRowModel().rows.map(row => ( 40 | <TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}> 41 | {row.getVisibleCells().map(cell => ( 42 | <TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell> 43 | ))} 44 | </TableRow> 45 | )) 46 | ) : ( 47 | <TableRow> 48 | <TableCell colSpan={columns.length} className="h-24 text-center"> 49 | No results. 50 | </TableCell> 51 | </TableRow> 52 | )} 53 | </TableBody> 54 | </Table> 55 | </div> 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /packages/ui/src/components/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react'; 2 | import { Avatar, AvatarFallback, AvatarImage } from '../ui'; 3 | const randomBgColor = [ 4 | '#ec6a5e', 5 | '#f5bd4f', 6 | '#61c455', 7 | '#3a82f7', 8 | '#7246e4', 9 | '#bef653', 10 | '#e97a35', 11 | '#4c9f54', 12 | '#3266e3', 13 | ]; 14 | 15 | interface ImageProps { 16 | src: string; 17 | value?: string; 18 | index?: number; 19 | } 20 | 21 | export const Image: FC<ImageProps> = ({ index, src, value }) => { 22 | const randomIndex = typeof index === 'number' ? index % randomBgColor.length : 0; 23 | return ( 24 | <div className="justify-center flex items-center " draggable={false}> 25 | <Avatar draggable={false} className=" h-5 w-5 inline-block mr-2 rounded-full"> 26 | <AvatarImage draggable={false} src={src} /> 27 | {value && typeof randomIndex === 'number' && ( 28 | <AvatarFallback 29 | delayMs={500} 30 | draggable={false} 31 | style={{ 32 | backgroundColor: randomBgColor[randomIndex], 33 | }} 34 | className=" text-white text-sm "> 35 | {value?.slice(0, 1).toLocaleUpperCase()} 36 | </AvatarFallback> 37 | )} 38 | </Avatar> 39 | </div> 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/ui/src/components/Spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@libs/utils'; 2 | import { VariantProps, cva } from 'class-variance-authority'; 3 | import { Loader } from 'lucide-react'; 4 | import React from 'react'; 5 | 6 | const spinnerVariants = cva( 7 | 'absolute bg-white/60 w-full h-full top-0 bottom-0 left-0 right-0 flex-col items-center justify-center', 8 | { 9 | variants: { 10 | show: { 11 | true: 'flex', 12 | false: 'hidden', 13 | }, 14 | }, 15 | defaultVariants: { 16 | show: true, 17 | }, 18 | }, 19 | ); 20 | 21 | const loaderVariants = cva('animate-spin text-primary', { 22 | variants: { 23 | size: { 24 | small: 'size-6', 25 | medium: 'size-8', 26 | large: 'size-12', 27 | }, 28 | }, 29 | defaultVariants: { 30 | size: 'medium', 31 | }, 32 | }); 33 | 34 | interface SpinnerContentProps extends VariantProps<typeof spinnerVariants>, VariantProps<typeof loaderVariants> { 35 | className?: string; 36 | children?: React.ReactNode; 37 | } 38 | 39 | export function Spinner({ size, show = true, children, className }: SpinnerContentProps) { 40 | return ( 41 | <> 42 | {children} 43 | <div className={spinnerVariants({ show })}> 44 | {show ? <Loader className={cn(loaderVariants({ size }), className)} /> : null} 45 | </div> 46 | </> 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /packages/ui/src/components/ThemeDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from 'lucide-react'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { 5 | DropdownMenu, 6 | DropdownMenuContent, 7 | DropdownMenuItem, 8 | DropdownMenuTrigger, 9 | } from '@/components/ui/dropdown-menu'; 10 | 11 | // import { useTheme } from "@/components/theme-provider" 12 | 13 | interface ThemeDropdownProps { 14 | setTheme: (theme: 'dark' | 'light' | 'system') => void; 15 | } 16 | 17 | export function ThemeDropdown(props: ThemeDropdownProps) { 18 | const { setTheme } = props; 19 | 20 | return ( 21 | <DropdownMenu> 22 | <DropdownMenuTrigger asChild> 23 | <Button variant="outline" size="icon"> 24 | <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> 25 | <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> 26 | <span className="sr-only">Toggle theme</span> 27 | </Button> 28 | </DropdownMenuTrigger> 29 | <DropdownMenuContent align="end"> 30 | <DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem> 31 | <DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem> 32 | <DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem> 33 | </DropdownMenuContent> 34 | </DropdownMenu> 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/ui/src/components/Tooltip/index.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as STooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; 2 | 3 | interface TooltipProps { 4 | children: React.ReactNode; 5 | title?: string | React.ReactNode; 6 | } 7 | 8 | const Tooltip = (props: TooltipProps) => { 9 | const { children, title } = props; 10 | return ( 11 | <TooltipProvider> 12 | <STooltip> 13 | <TooltipTrigger>{children}</TooltipTrigger> 14 | <TooltipContent> 15 | <p>{title}</p> 16 | </TooltipContent> 17 | </STooltip> 18 | </TooltipProvider> 19 | ); 20 | }; 21 | 22 | export default Tooltip; 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DateTable'; 2 | export * from './Image'; 3 | export * from './Spinner'; 4 | export * from './ThemeDropdown'; 5 | export * from './ui'; 6 | 7 | import { default as SyncTooltip } from './Tooltip'; 8 | // import from './Tooltip'; 9 | export { SyncTooltip }; 10 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@libs/utils'; 5 | import { buttonVariants } from 'src/components/ui/button'; 6 | 7 | const AlertDialog = AlertDialogPrimitive.Root; 8 | 9 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 10 | 11 | const AlertDialogPortal = AlertDialogPrimitive.Portal; 12 | 13 | const AlertDialogOverlay = React.forwardRef< 14 | React.ElementRef<typeof AlertDialogPrimitive.Overlay>, 15 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay> 16 | >(({ className, ...props }, ref) => ( 17 | <AlertDialogPrimitive.Overlay 18 | className={cn( 19 | 'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 20 | className, 21 | )} 22 | {...props} 23 | ref={ref} 24 | /> 25 | )); 26 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 27 | 28 | const AlertDialogContent = React.forwardRef< 29 | React.ElementRef<typeof AlertDialogPrimitive.Content>, 30 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> 31 | >(({ className, ...props }, ref) => ( 32 | <AlertDialogPortal> 33 | <AlertDialogOverlay /> 34 | <AlertDialogPrimitive.Content 35 | ref={ref} 36 | className={cn( 37 | 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', 38 | className, 39 | )} 40 | {...props} 41 | /> 42 | </AlertDialogPortal> 43 | )); 44 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 45 | 46 | const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( 47 | <div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} /> 48 | ); 49 | AlertDialogHeader.displayName = 'AlertDialogHeader'; 50 | 51 | const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( 52 | <div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} /> 53 | ); 54 | AlertDialogFooter.displayName = 'AlertDialogFooter'; 55 | 56 | const AlertDialogTitle = React.forwardRef< 57 | React.ElementRef<typeof AlertDialogPrimitive.Title>, 58 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title> 59 | >(({ className, ...props }, ref) => ( 60 | <AlertDialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold', className)} {...props} /> 61 | )); 62 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 63 | 64 | const AlertDialogDescription = React.forwardRef< 65 | React.ElementRef<typeof AlertDialogPrimitive.Description>, 66 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description> 67 | >(({ className, ...props }, ref) => ( 68 | <AlertDialogPrimitive.Description ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> 69 | )); 70 | AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; 71 | 72 | const AlertDialogAction = React.forwardRef< 73 | React.ElementRef<typeof AlertDialogPrimitive.Action>, 74 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action> 75 | >(({ className, ...props }, ref) => ( 76 | <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} /> 77 | )); 78 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 79 | 80 | const AlertDialogCancel = React.forwardRef< 81 | React.ElementRef<typeof AlertDialogPrimitive.Cancel>, 82 | React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel> 83 | >(({ className, ...props }, ref) => ( 84 | <AlertDialogPrimitive.Cancel 85 | ref={ref} 86 | className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)} 87 | {...props} 88 | /> 89 | )); 90 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 91 | 92 | export { 93 | AlertDialog, AlertDialogAction, 94 | AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger 95 | }; 96 | 97 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from 'class-variance-authority'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@libs/utils'; 5 | 6 | const alertVariants = cva( 7 | 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground', 8 | { 9 | variants: { 10 | variant: { 11 | default: 'bg-background text-foreground', 12 | destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive', 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: 'default', 17 | }, 18 | }, 19 | ); 20 | 21 | const Alert = React.forwardRef< 22 | HTMLDivElement, 23 | React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants> 24 | >(({ className, variant, ...props }, ref) => ( 25 | <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} /> 26 | )); 27 | Alert.displayName = 'Alert'; 28 | 29 | const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( 30 | ({ className, ...props }, ref) => ( 31 | <h5 ref={ref} className={cn('mb-1 font-medium leading-none tracking-tight', className)} {...props} /> 32 | ), 33 | ); 34 | AlertTitle.displayName = 'AlertTitle'; 35 | 36 | const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>( 37 | ({ className, ...props }, ref) => ( 38 | <div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} /> 39 | ), 40 | ); 41 | AlertDescription.displayName = 'AlertDescription'; 42 | 43 | export { Alert, AlertDescription, AlertTitle }; 44 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@libs/utils'; 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef<typeof AvatarPrimitive.Root>, 8 | React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> 9 | >(({ className, ...props }, ref) => ( 10 | <AvatarPrimitive.Root 11 | ref={ref} 12 | className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)} 13 | {...props} 14 | /> 15 | )); 16 | Avatar.displayName = AvatarPrimitive.Root.displayName; 17 | 18 | const AvatarImage = React.forwardRef< 19 | React.ElementRef<typeof AvatarPrimitive.Image>, 20 | React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> 21 | >(({ className, ...props }, ref) => ( 22 | <AvatarPrimitive.Image ref={ref} className={cn('aspect-square h-full w-full', className)} {...props} /> 23 | )); 24 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 25 | 26 | const AvatarFallback = React.forwardRef< 27 | React.ElementRef<typeof AvatarPrimitive.Fallback>, 28 | React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> 29 | >(({ className, ...props }, ref) => ( 30 | <AvatarPrimitive.Fallback 31 | ref={ref} 32 | className={cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className)} 33 | {...props} 34 | /> 35 | )); 36 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 37 | 38 | export { Avatar, AvatarFallback, AvatarImage }; 39 | 40 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@libs/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 14 | outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 15 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 16 | ghost: 'hover:bg-accent hover:text-accent-foreground', 17 | link: 'text-primary underline-offset-4 hover:underline', 18 | }, 19 | size: { 20 | default: 'h-10 px-4 py-2', 21 | sm: 'h-9 rounded-md px-3', 22 | lg: 'h-11 rounded-md px-8', 23 | icon: 'h-10 w-10', 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: 'default', 28 | size: 'default', 29 | }, 30 | }, 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes<HTMLButtonElement>, 35 | VariantProps<typeof buttonVariants> { 36 | asChild?: boolean; 37 | } 38 | 39 | const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 40 | ({ className, variant, size, asChild = false, ...props }, ref) => { 41 | const Comp = asChild ? Slot : 'button'; 42 | return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />; 43 | }, 44 | ); 45 | Button.displayName = 'Button'; 46 | 47 | export { Button, buttonVariants }; 48 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@libs/utils'; 4 | 5 | const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => ( 6 | <div ref={ref} className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)} {...props} /> 7 | )); 8 | Card.displayName = 'Card'; 9 | 10 | const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( 11 | ({ className, ...props }, ref) => ( 12 | <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} /> 13 | ), 14 | ); 15 | CardHeader.displayName = 'CardHeader'; 16 | 17 | const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( 18 | ({ className, ...props }, ref) => ( 19 | <h3 ref={ref} className={cn('text-2xl font-semibold leading-none tracking-tight', className)} {...props} /> 20 | ), 21 | ); 22 | CardTitle.displayName = 'CardTitle'; 23 | 24 | const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>( 25 | ({ className, ...props }, ref) => ( 26 | <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} /> 27 | ), 28 | ); 29 | CardDescription.displayName = 'CardDescription'; 30 | 31 | const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( 32 | ({ className, ...props }, ref) => <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />, 33 | ); 34 | CardContent.displayName = 'CardContent'; 35 | 36 | const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( 37 | ({ className, ...props }, ref) => ( 38 | <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} /> 39 | ), 40 | ); 41 | CardFooter.displayName = 'CardFooter'; 42 | 43 | export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; 44 | 45 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert'; 2 | export * from './button'; 3 | 4 | export * from './avatar'; 5 | export * from './card'; 6 | export * from './dropdown-menu'; 7 | export * from './input'; 8 | export * from './label'; 9 | export * from './sonner'; 10 | export * from './switch'; 11 | // export * from './table'; 12 | export * from './alert-dialog'; 13 | export * from './popover'; 14 | export * from './tooltip'; 15 | 16 | export * from './select'; 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@libs/utils'; 4 | 5 | export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} 6 | 7 | const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => { 8 | return ( 9 | <input 10 | type={type} 11 | className={cn( 12 | 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', 13 | className, 14 | )} 15 | ref={ref} 16 | {...props} 17 | /> 18 | ); 19 | }); 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@libs/utils'; 6 | 7 | const labelVariants = cva('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'); 8 | 9 | const Label = React.forwardRef< 10 | React.ElementRef<typeof LabelPrimitive.Root>, 11 | React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> 12 | >(({ className, ...props }, ref) => ( 13 | <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} /> 14 | )); 15 | Label.displayName = LabelPrimitive.Root.displayName; 16 | 17 | export { Label }; 18 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@libs/utils'; 5 | 6 | const Popover = PopoverPrimitive.Root; 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger; 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef<typeof PopoverPrimitive.Content>, 12 | React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 13 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 14 | <PopoverPrimitive.Portal> 15 | <PopoverPrimitive.Content 16 | ref={ref} 17 | align={align} 18 | sideOffset={sideOffset} 19 | className={cn( 20 | 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', 21 | className, 22 | )} 23 | {...props} 24 | /> 25 | </PopoverPrimitive.Portal> 26 | )); 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 28 | 29 | export { Popover, PopoverContent, PopoverTrigger }; 30 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as SelectPrimitive from '@radix-ui/react-select'; 2 | import { Check, ChevronDown, ChevronUp } from 'lucide-react'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@libs/utils'; 6 | 7 | const Select = SelectPrimitive.Root; 8 | const SelectPortal = SelectPrimitive.Portal; 9 | 10 | const SelectGroup = SelectPrimitive.Group; 11 | 12 | const SelectValue = SelectPrimitive.Value; 13 | 14 | const SelectTrigger = React.forwardRef< 15 | React.ElementRef<typeof SelectPrimitive.Trigger>, 16 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> 17 | >(({ className, children, ...props }, ref) => ( 18 | <SelectPrimitive.Trigger 19 | ref={ref} 20 | className={cn( 21 | 'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', 22 | className, 23 | )} 24 | {...props}> 25 | {children} 26 | <SelectPrimitive.Icon asChild> 27 | <ChevronDown className="h-4 w-4 opacity-50" /> 28 | </SelectPrimitive.Icon> 29 | </SelectPrimitive.Trigger> 30 | )); 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; 32 | 33 | const SelectScrollUpButton = React.forwardRef< 34 | React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, 35 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> 36 | >(({ className, ...props }, ref) => ( 37 | <SelectPrimitive.ScrollUpButton 38 | ref={ref} 39 | className={cn('flex cursor-default items-center justify-center py-1', className)} 40 | {...props}> 41 | <ChevronUp className="h-4 w-4" /> 42 | </SelectPrimitive.ScrollUpButton> 43 | )); 44 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; 45 | 46 | const SelectScrollDownButton = React.forwardRef< 47 | React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, 48 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> 49 | >(({ className, ...props }, ref) => ( 50 | <SelectPrimitive.ScrollDownButton 51 | ref={ref} 52 | className={cn('flex cursor-default items-center justify-center py-1', className)} 53 | {...props}> 54 | <ChevronDown className="h-4 w-4" /> 55 | </SelectPrimitive.ScrollDownButton> 56 | )); 57 | SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; 58 | 59 | const SelectContent = React.forwardRef< 60 | React.ElementRef<typeof SelectPrimitive.Content>, 61 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> 62 | >(({ className, children, position = 'popper', ...props }, ref) => ( 63 | <SelectPrimitive.Portal> 64 | <SelectPrimitive.Content 65 | ref={ref} 66 | className={cn( 67 | 'relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]', 68 | position === 'popper' && 69 | 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', 70 | className, 71 | )} 72 | position={position} 73 | {...props}> 74 | <SelectScrollUpButton /> 75 | <SelectPrimitive.Viewport 76 | className={cn( 77 | 'p-1', 78 | position === 'popper' && 79 | 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]', 80 | )}> 81 | {children} 82 | </SelectPrimitive.Viewport> 83 | <SelectScrollDownButton /> 84 | </SelectPrimitive.Content> 85 | </SelectPrimitive.Portal> 86 | )); 87 | SelectContent.displayName = SelectPrimitive.Content.displayName; 88 | 89 | const SelectLabel = React.forwardRef< 90 | React.ElementRef<typeof SelectPrimitive.Label>, 91 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> 92 | >(({ className, ...props }, ref) => ( 93 | <SelectPrimitive.Label ref={ref} className={cn('py-1.5 pl-8 pr-2 text-sm font-semibold', className)} {...props} /> 94 | )); 95 | SelectLabel.displayName = SelectPrimitive.Label.displayName; 96 | 97 | const SelectItem = React.forwardRef< 98 | React.ElementRef<typeof SelectPrimitive.Item>, 99 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> 100 | >(({ className, children, ...props }, ref) => ( 101 | <SelectPrimitive.Item 102 | ref={ref} 103 | className={cn( 104 | 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 105 | className, 106 | )} 107 | {...props}> 108 | <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> 109 | <SelectPrimitive.ItemIndicator> 110 | <Check className="h-4 w-4" /> 111 | </SelectPrimitive.ItemIndicator> 112 | </span> 113 | 114 | <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> 115 | </SelectPrimitive.Item> 116 | )); 117 | SelectItem.displayName = SelectPrimitive.Item.displayName; 118 | 119 | const SelectSeparator = React.forwardRef< 120 | React.ElementRef<typeof SelectPrimitive.Separator>, 121 | React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> 122 | >(({ className, ...props }, ref) => ( 123 | <SelectPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} /> 124 | )); 125 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName; 126 | 127 | export { 128 | Select, 129 | SelectContent, 130 | SelectGroup, 131 | SelectItem, 132 | SelectLabel, 133 | SelectPortal, 134 | SelectScrollDownButton, 135 | SelectScrollUpButton, 136 | SelectSeparator, 137 | SelectTrigger, 138 | SelectValue 139 | }; 140 | 141 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from "next-themes" 2 | import { Toaster as Sonner } from "sonner" 3 | 4 | type ToasterProps = React.ComponentProps<typeof Sonner> 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | <Sonner 11 | theme={theme as ToasterProps["theme"]} 12 | className="toaster group" 13 | toastOptions={{ 14 | classNames: { 15 | toast: 16 | "group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg", 17 | description: "group-[.toast]:text-muted-foreground", 18 | actionButton: 19 | "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground", 20 | cancelButton: 21 | "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground", 22 | }, 23 | }} 24 | {...props} 25 | /> 26 | ) 27 | } 28 | 29 | export { Toaster } 30 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@libs/utils'; 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef<typeof SwitchPrimitives.Root>, 8 | React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> 9 | >(({ className, ...props }, ref) => ( 10 | <SwitchPrimitives.Root 11 | className={cn( 12 | 'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input', 13 | className, 14 | )} 15 | {...props} 16 | ref={ref}> 17 | <SwitchPrimitives.Thumb 18 | className={cn( 19 | 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0', 20 | )} 21 | /> 22 | </SwitchPrimitives.Root> 23 | )); 24 | Switch.displayName = SwitchPrimitives.Root.displayName; 25 | 26 | export { Switch }; 27 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@libs/utils'; 4 | 5 | const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>( 6 | ({ className, ...props }, ref) => ( 7 | <div className="relative w-full overflow-auto"> 8 | <table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} /> 9 | </div> 10 | ), 11 | ); 12 | Table.displayName = 'Table'; 13 | 14 | const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>( 15 | ({ className, ...props }, ref) => <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />, 16 | ); 17 | TableHeader.displayName = 'TableHeader'; 18 | 19 | const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>( 20 | ({ className, ...props }, ref) => ( 21 | <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} /> 22 | ), 23 | ); 24 | TableBody.displayName = 'TableBody'; 25 | 26 | const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>( 27 | ({ className, ...props }, ref) => ( 28 | <tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} /> 29 | ), 30 | ); 31 | TableFooter.displayName = 'TableFooter'; 32 | 33 | const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>( 34 | ({ className, ...props }, ref) => ( 35 | <tr 36 | ref={ref} 37 | className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)} 38 | {...props} 39 | /> 40 | ), 41 | ); 42 | TableRow.displayName = 'TableRow'; 43 | 44 | const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>( 45 | ({ className, ...props }, ref) => ( 46 | <th 47 | ref={ref} 48 | className={cn( 49 | 'h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', 50 | className, 51 | )} 52 | {...props} 53 | /> 54 | ), 55 | ); 56 | TableHead.displayName = 'TableHead'; 57 | 58 | const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>( 59 | ({ className, ...props }, ref) => ( 60 | <td ref={ref} className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className)} {...props} /> 61 | ), 62 | ); 63 | TableCell.displayName = 'TableCell'; 64 | 65 | const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>( 66 | ({ className, ...props }, ref) => ( 67 | <caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} /> 68 | ), 69 | ); 70 | TableCaption.displayName = 'TableCaption'; 71 | 72 | export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }; 73 | -------------------------------------------------------------------------------- /packages/ui/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@libs/utils'; 5 | 6 | const TooltipProvider = TooltipPrimitive.Provider; 7 | 8 | const Tooltip = TooltipPrimitive.Root; 9 | 10 | const TooltipTrigger = TooltipPrimitive.Trigger; 11 | 12 | const TooltipContent = React.forwardRef< 13 | React.ElementRef<typeof TooltipPrimitive.Content>, 14 | React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> 15 | >(({ className, sideOffset = 4, ...props }, ref) => ( 16 | <TooltipPrimitive.Content 17 | ref={ref} 18 | sideOffset={sideOffset} 19 | className={cn( 20 | 'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', 21 | className, 22 | )} 23 | {...props} 24 | /> 25 | )); 26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 27 | 28 | export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; 29 | 30 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './libs/index'; 3 | 4 | -------------------------------------------------------------------------------- /packages/ui/src/libs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | -------------------------------------------------------------------------------- /packages/ui/src/libs/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require('@sync-your-cookie/tailwindcss-config'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | ...baseConfig, 6 | darkMode: ["class"], 7 | content: [ 8 | './pages/**/*.{ts,tsx}', 9 | './components/**/*.{ts,tsx}', 10 | './app/**/*.{ts,tsx}', 11 | './src/**/*.{ts,tsx}', 12 | `node_modules/@sync-your-cookie/ui/**/*.{js,ts,jsx,tsx,mdx}` 13 | ], 14 | prefix: "", 15 | theme: { 16 | container: { 17 | center: true, 18 | padding: "2rem", 19 | screens: { 20 | "2xl": "1400px", 21 | }, 22 | }, 23 | extend: { 24 | colors: { 25 | border: "hsl(var(--border))", 26 | input: "hsl(var(--input))", 27 | ring: "hsl(var(--ring))", 28 | background: "hsl(var(--background))", 29 | foreground: "hsl(var(--foreground))", 30 | primary: { 31 | DEFAULT: "hsl(var(--primary))", 32 | foreground: "hsl(var(--primary-foreground))", 33 | }, 34 | secondary: { 35 | DEFAULT: "hsl(var(--secondary))", 36 | foreground: "hsl(var(--secondary-foreground))", 37 | }, 38 | destructive: { 39 | DEFAULT: "hsl(var(--destructive))", 40 | foreground: "hsl(var(--destructive-foreground))", 41 | }, 42 | muted: { 43 | DEFAULT: "hsl(var(--muted))", 44 | foreground: "hsl(var(--muted-foreground))", 45 | }, 46 | accent: { 47 | DEFAULT: "hsl(var(--accent))", 48 | foreground: "hsl(var(--accent-foreground))", 49 | }, 50 | popover: { 51 | DEFAULT: "hsl(var(--popover))", 52 | foreground: "hsl(var(--popover-foreground))", 53 | }, 54 | card: { 55 | DEFAULT: "hsl(var(--card))", 56 | foreground: "hsl(var(--card-foreground))", 57 | }, 58 | }, 59 | borderRadius: { 60 | lg: "var(--radius)", 61 | md: "calc(var(--radius) - 2px)", 62 | sm: "calc(var(--radius) - 4px)", 63 | }, 64 | keyframes: { 65 | "accordion-down": { 66 | from: { height: "0" }, 67 | to: { height: "var(--radix-accordion-content-height)" }, 68 | }, 69 | "accordion-up": { 70 | from: { height: "var(--radix-accordion-content-height)" }, 71 | to: { height: "0" }, 72 | }, 73 | }, 74 | animation: { 75 | "accordion-down": "accordion-down 0.2s ease-out", 76 | "accordion-up": "accordion-up 0.2s ease-out", 77 | }, 78 | }, 79 | }, 80 | plugins: [require("tailwindcss-animate")], 81 | } 82 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "jsx": "react-jsx", 8 | "baseUrl": ".", 9 | "paths": { 10 | "src/*": ["./src/*"], 11 | "@components/*": ["./src/components/*"], 12 | "@/components/*": ["./src/components/*"], 13 | "@libs/*": ["./src/libs/*"] 14 | }, 15 | "types": ["chrome"], 16 | }, 17 | "include": ["src/**/*"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | treeshake: true, 5 | splitting: true, 6 | entry: ['src/index.ts'], 7 | format: ['cjs', 'esm'], 8 | dts: true, 9 | clean: true, 10 | external: ['chrome'], 11 | }); 12 | -------------------------------------------------------------------------------- /packages/zipper/index.mts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import { zipBundle } from './lib/index.js'; 4 | const IS_FIREFOX = process.env.BROWSER === 'firefox'; 5 | const packageJson = JSON.parse(fs.readFileSync('../../package.json', 'utf8')); 6 | 7 | const YYYY_MM_DD = new Date().toISOString().slice(0, 10).replace(/-/g, ''); 8 | const HH_mm_ss = new Date().toISOString().slice(11, 19).replace(/:/g, ''); 9 | 10 | const fileName = `extension-${packageJson.version}-${YYYY_MM_DD}-${HH_mm_ss}`; 11 | 12 | await zipBundle({ 13 | distDirectory: resolve(import.meta.dirname, '..', '..', '..', 'dist'), 14 | buildDirectory: resolve(import.meta.dirname, '..', '..', '..', 'dist-zip'), 15 | archiveName: IS_FIREFOX ? `${fileName}.xpi` : `${fileName}.zip`, 16 | }).catch(error => { 17 | console.error('Error zipping the bundle:', error); 18 | process.exit(1); 19 | }); 20 | 21 | console.log('Zipping completed'); 22 | -------------------------------------------------------------------------------- /packages/zipper/lib/index.ts: -------------------------------------------------------------------------------- 1 | import fg from 'fast-glob'; 2 | import { AsyncZipDeflate, Zip } from 'fflate'; 3 | import { createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs'; 4 | import { posix, resolve } from 'node:path'; 5 | 6 | // Converts bytes to megabytes 7 | function toMB(bytes: number): number { 8 | return bytes / 1024 / 1024; 9 | } 10 | 11 | // Creates the build directory if it doesn't exist 12 | function ensureBuildDirectoryExists(buildDirectory: string): void { 13 | if (!existsSync(buildDirectory)) { 14 | mkdirSync(buildDirectory, { recursive: true }); 15 | } 16 | } 17 | 18 | // Logs the package size and duration 19 | function logPackageSize(size: number, startTime: number): void { 20 | console.log(`Zip Package size: ${toMB(size).toFixed(2)} MB in ${Date.now() - startTime}ms`); 21 | } 22 | 23 | // Handles file streaming and zipping 24 | function streamFileToZip( 25 | absPath: string, 26 | relPath: string, 27 | zip: Zip, 28 | onAbort: () => void, 29 | onError: (error: Error) => void, 30 | ): void { 31 | const data = new AsyncZipDeflate(relPath, { level: 9 }); 32 | zip.add(data); 33 | 34 | createReadStream(absPath) 35 | .on('data', (chunk: string | Buffer) => 36 | typeof chunk === 'string' ? data.push(Buffer.from(chunk), false) : data.push(chunk, false), 37 | ) 38 | .on('end', () => data.push(new Uint8Array(0), true)) 39 | .on('error', error => { 40 | onAbort(); 41 | onError(error); 42 | }); 43 | } 44 | 45 | // Zips the bundle 46 | export const zipBundle = async ( 47 | { 48 | distDirectory, 49 | buildDirectory, 50 | archiveName, 51 | }: { 52 | distDirectory: string; 53 | buildDirectory: string; 54 | archiveName: string; 55 | }, 56 | withMaps = false, 57 | ): Promise<void> => { 58 | ensureBuildDirectoryExists(buildDirectory); 59 | 60 | const zipFilePath = resolve(buildDirectory, archiveName); 61 | const output = createWriteStream(zipFilePath); 62 | 63 | const fileList = await fg( 64 | [ 65 | '**/*', // Pick all nested files 66 | ...(!withMaps ? ['!**/(*.js.map|*.css.map)'] : []), // Exclude source maps conditionally 67 | ], 68 | { 69 | cwd: distDirectory, 70 | onlyFiles: true, 71 | }, 72 | ); 73 | 74 | return new Promise<void>((pResolve, pReject) => { 75 | let aborted = false; 76 | let totalSize = 0; 77 | const timer = Date.now(); 78 | const zip = new Zip((err, data, final) => { 79 | if (err) { 80 | pReject(err); 81 | } else { 82 | totalSize += data.length; 83 | output.write(data); 84 | if (final) { 85 | logPackageSize(totalSize, timer); 86 | output.end(); 87 | pResolve(); 88 | } 89 | } 90 | }); 91 | 92 | // Handle file read streams 93 | for (const file of fileList) { 94 | if (aborted) return; 95 | 96 | const absPath = resolve(distDirectory, file); 97 | const absPosixPath = posix.resolve(distDirectory, file); 98 | const relPosixPath = posix.relative(distDirectory, absPosixPath); 99 | 100 | console.log(`Adding file: ${relPosixPath}`); 101 | streamFileToZip( 102 | absPath, 103 | relPosixPath, 104 | zip, 105 | () => { 106 | aborted = true; 107 | zip.terminate(); 108 | }, 109 | error => pReject(`Error reading file ${absPath}: ${error.message}`), 110 | ); 111 | } 112 | 113 | zip.end(); 114 | }); 115 | }; 116 | -------------------------------------------------------------------------------- /packages/zipper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/zipper", 3 | "version": "0.8.0", 4 | "description": "chrome extension - zipper", 5 | "type": "module", 6 | "private": true, 7 | "sideEffects": false, 8 | "files": [ 9 | "dist/**" 10 | ], 11 | "types": "index.mts", 12 | "main": "dist/index.mjs", 13 | "scripts": { 14 | "clean:bundle": "rimraf dist", 15 | "clean:node_modules": "pnpx rimraf node_modules", 16 | "clean:turbo": "rimraf .turbo", 17 | "clean": "pnpm clean:bundle && pnpm clean:node_modules && pnpm clean:turbo", 18 | "zip": "npm run ready && node dist/index.mjs", 19 | "lint": "eslint .", 20 | "ready": "tsc -b", 21 | "lint:fix": "pnpm lint --fix", 22 | "prettier": "prettier . --write --ignore-path ../../.prettierignore", 23 | "type-check": "tsc --noEmit" 24 | }, 25 | "devDependencies": { 26 | "@sync-your-cookie/tsconfig": "workspace:*", 27 | "fflate": "^0.8.2", 28 | "fast-glob": "^3.3.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/zipper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/utils", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "dist", 6 | "module": "esnext", 7 | "target": "esnext" 8 | }, 9 | "include": ["index.mts", "lib"] 10 | } 11 | -------------------------------------------------------------------------------- /pages/options/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <title>SyncYourCookie-Options</title> 6 | </head> 7 | 8 | <body> 9 | <div id="app-container"></div> 10 | <script type="module" src="./src/index.tsx"></script> 11 | </body> 12 | </html> 13 | -------------------------------------------------------------------------------- /pages/options/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/options", 3 | "version": "0.0.1", 4 | "description": "chrome extension options", 5 | "private": true, 6 | "sideEffects": true, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf ./dist && rimraf .turbo", 12 | "build": "pnpm run clean && tsc --noEmit && vite build", 13 | "build:watch": "cross-env __DEV__=true vite build -w --mode development", 14 | "dev": "pnpm build:watch", 15 | "lint": "eslint . --ext .ts,.tsx", 16 | "lint:fix": "pnpm lint --fix", 17 | "prettier": "prettier . --write", 18 | "type-check": "tsc --noEmit" 19 | }, 20 | "dependencies": { 21 | "@sync-your-cookie/shared": "workspace:*", 22 | "@sync-your-cookie/storage": "workspace:*", 23 | "lucide-react": "^0.394.0", 24 | "sonner": "^1.5.0" 25 | }, 26 | "devDependencies": { 27 | "@sync-your-cookie/hmr": "workspace:*", 28 | "@sync-your-cookie/tailwindcss-config": "workspace:*", 29 | "@sync-your-cookie/tsconfig": "workspace:*", 30 | "@sync-your-cookie/ui": "workspace:*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/options/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /pages/options/public/github.svg: -------------------------------------------------------------------------------- 1 | <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg> -------------------------------------------------------------------------------- /pages/options/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/pages/options/public/logo.png -------------------------------------------------------------------------------- /pages/options/src/components/SettingsPopover.tsx: -------------------------------------------------------------------------------- 1 | import { useStorageSuspense } from '@sync-your-cookie/shared'; 2 | import { domainStatusStorage } from '@sync-your-cookie/storage/lib/domainStatusStorage'; 3 | import { settingsStorage } from '@sync-your-cookie/storage/lib/settingsStorage'; 4 | 5 | import { pullCookies } from '@sync-your-cookie/shared'; 6 | import { cookieStorage } from '@sync-your-cookie/storage/lib/cookieStorage'; 7 | import { Label, Popover, PopoverContent, PopoverTrigger, Switch } from '@sync-your-cookie/ui'; 8 | import React, { useEffect, useState } from 'react'; 9 | import { StorageSelect } from './StorageSelect'; 10 | interface SettingsPopover { 11 | trigger: React.ReactNode; 12 | } 13 | 14 | export function SettingsPopover({ trigger }: SettingsPopover) { 15 | const settingsInfo = useStorageSuspense(settingsStorage); 16 | const [selectOpen, setSelectOpen] = useState(false); 17 | 18 | const handleCheckChange = (checked: boolean) => { 19 | settingsStorage.update({ 20 | protobufEncoding: checked, 21 | }); 22 | }; 23 | 24 | const handleValueChange = (value: string) => { 25 | settingsStorage.update({ 26 | storageKey: value, 27 | }); 28 | }; 29 | 30 | const reset = async () => { 31 | await domainStatusStorage.resetState(); 32 | await cookieStorage.reset(); 33 | await pullCookies(); 34 | console.log("reset finished"); 35 | } 36 | 37 | useEffect(()=> { 38 | reset(); 39 | }, [settingsInfo.storageKey]) 40 | 41 | const handleOpenChange = (open: boolean) => { 42 | if (selectOpen) return; 43 | console.log('popover open', open); 44 | // if (open === false && (settingsInfo.storageKey !== storageKey || !storageKey)) { 45 | // console.log('open', open); 46 | // settingsStorage.update({ 47 | // storageKey: storageKey || defaultKey, 48 | // }); 49 | // domainConfigStorage.resetState(); 50 | // } 51 | }; 52 | 53 | const handleSelectOpenChange = (open: boolean) => { 54 | console.log('select open', open); 55 | setSelectOpen(open); 56 | }; 57 | 58 | const handleAddStorageKey = async (key: string) => { 59 | await settingsStorage.addStorageKey(key); 60 | }; 61 | 62 | const handleRemoveStorageKey = async (key: string) => { 63 | // if (settingsInfo.storageKey === key) { 64 | // settingsStorage.update({ 65 | // storageKey: defaultKey, 66 | // }); 67 | // setStorageKey(defaultKey); 68 | // } 69 | await settingsStorage.removeStorageKey(key); 70 | }; 71 | 72 | return ( 73 | <Popover onOpenChange={handleOpenChange}> 74 | <PopoverTrigger asChild>{trigger}</PopoverTrigger> 75 | <PopoverContent className="w-[320px]"> 76 | <div className="grid gap-4"> 77 | <div className="space-y-2"> 78 | <h3 className="leading-none font-medium text-base">Storage Settings</h3> 79 | <p className="text-muted-foreground text-sm">Set to how to store in cloudflare KV.</p> 80 | </div> 81 | <div className="gap-2"> 82 | <div className="flex items-center gap-4 mb-4"> 83 | <Label className="w-[116px] block text-right" htmlFor="storage-key"> 84 | Storage Key 85 | </Label> 86 | {/* <Input 87 | onChange={handleKeyInputChange} 88 | id="storage-key" 89 | value={storageKey} 90 | className="h-8 flex-1" 91 | placeholder={defaultKey} 92 | /> */} 93 | <StorageSelect 94 | options={settingsInfo.storageKeyList} 95 | open={selectOpen} 96 | onOpenChange={handleSelectOpenChange} 97 | value={settingsInfo.storageKey || ''} 98 | onAdd={handleAddStorageKey} 99 | onRemove={handleRemoveStorageKey} 100 | onValueChange={handleValueChange} 101 | /> 102 | </div> 103 | <div className="flex items-center gap-4"> 104 | <Label className="whitespace-nowrap block w-[116px] text-right" htmlFor="encoding"> 105 | Protobuf Encoding 106 | </Label> 107 | <Switch onCheckedChange={handleCheckChange} checked={settingsInfo.protobufEncoding} id="encoding" /> 108 | </div> 109 | </div> 110 | </div> 111 | </PopoverContent> 112 | </Popover> 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /pages/options/src/components/StorageSelect.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Input, 4 | Select, 5 | SelectContent, 6 | SelectItem, 7 | SelectPortal, 8 | SelectTrigger, 9 | SelectValue, 10 | } from '@sync-your-cookie/ui'; 11 | import { useRef, useState } from 'react'; 12 | 13 | import { CircleX, Plus } from 'lucide-react'; 14 | import { toast } from 'sonner'; 15 | 16 | interface StorageSelectProps extends React.ComponentProps<typeof Select> { 17 | options: string[] 18 | value: string 19 | onAdd: (key:string)=> void 20 | onRemove: (key: string) => void; 21 | } 22 | 23 | export function StorageSelect(props: StorageSelectProps) { 24 | const { value, onRemove, options, onValueChange, ...rest } = props; 25 | const [inputValue, setInputValue] = useState(''); 26 | const containerRef = useRef<HTMLDivElement>(null); 27 | 28 | const handleAdd = () => { 29 | const newKey = inputValue.trim().replaceAll(/\s+/g, ''); 30 | if(options.includes(newKey)) { 31 | console.warn('Key already exists or is empty'); 32 | toast.error('Key already exists'); 33 | return; 34 | } 35 | props.onAdd(newKey); 36 | setInputValue(''); 37 | } 38 | 39 | const handleRemoveKey = (key: string) => { 40 | // Handle removing a storage key 41 | console.log('Remove storage key', key); 42 | onRemove(key); 43 | }; 44 | return ( 45 | <div ref={containerRef}> 46 | <Select value={value} onValueChange={(val) => { 47 | if(val === value) { 48 | return; 49 | } 50 | onValueChange?.(val); 51 | 52 | }} {...rest}> 53 | <SelectTrigger className="w-[160px] scale-90 "> 54 | <SelectValue className="ml-[-8px]" placeholder="Select a Storage Key" /> 55 | </SelectTrigger> 56 | <SelectPortal > 57 | <SelectContent onCloseAutoFocus={(evt) => evt.preventDefault()} > 58 | { 59 | options.map((item, index) => { 60 | return <div key={item} className='relative group'> 61 | <SelectItem className=" w-full" value={item}> 62 | <span className='cursor-pointer'>{item}</span> 63 | </SelectItem> 64 | { 65 | options.length > 1 && item !== value ? <span 66 | ref={containerRef} 67 | onClick={(e) => handleRemoveKey(item)} 68 | role="button" 69 | tabIndex={index} 70 | className="absolute top-2 invisible right-[6px] cursor-pointer group-hover:visible"> 71 | <CircleX size={18} /> 72 | </span> : null 73 | } 74 | 75 | </div> 76 | }) 77 | } 78 | 79 | <div className="flex mx-2 items-center mt-2 gap-2"> 80 | <Input value={inputValue} onChange={(event)=> {setInputValue(event?.target.value.replaceAll(/\s+/g, ''))}} className="h-8 " /> 81 | <Button disabled={!inputValue.replaceAll(/\s+/g, '')} onClick={()=> handleAdd()} className="ml-0 scale-90" size="sm" type="submit" variant="outline"> 82 | <Plus size={18} /> 83 | Add 84 | </Button> 85 | </div> 86 | </SelectContent> 87 | </SelectPortal> 88 | </Select> 89 | </div> 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /pages/options/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 5 | 'Droid Sans', 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | -------------------------------------------------------------------------------- /pages/options/src/index.tsx: -------------------------------------------------------------------------------- 1 | import Options from '@src/Options'; 2 | import '@src/index.css'; 3 | import { ThemeProvider } from '@sync-your-cookie/shared'; 4 | import '@sync-your-cookie/ui/css'; 5 | import { createRoot } from 'react-dom/client'; 6 | 7 | function init() { 8 | const appContainer = document.querySelector('#app-container'); 9 | if (!appContainer) { 10 | throw new Error('Can not find #app-container'); 11 | } 12 | const root = createRoot(appContainer); 13 | root.render( 14 | <ThemeProvider> 15 | <Options /> 16 | </ThemeProvider>, 17 | ); 18 | } 19 | 20 | init(); 21 | -------------------------------------------------------------------------------- /pages/options/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const uiConfig = require('@sync-your-cookie/ui/tailwind.config'); 2 | 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | module.exports = { 6 | ...uiConfig, 7 | }; 8 | -------------------------------------------------------------------------------- /pages/options/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@src/*": ["src/*"] 7 | }, 8 | "jsx": "react-jsx", 9 | "types": ["chrome"] 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /pages/options/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import { resolve } from 'path'; 4 | import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; 5 | 6 | const rootDir = resolve(__dirname); 7 | const srcDir = resolve(rootDir, 'src'); 8 | 9 | const isDev = process.env.__DEV__ === 'true'; 10 | const isProduction = !isDev; 11 | 12 | export default defineConfig({ 13 | resolve: { 14 | alias: { 15 | '@src': srcDir, 16 | }, 17 | }, 18 | base: '', 19 | plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], 20 | publicDir: resolve(rootDir, 'public'), 21 | build: { 22 | outDir: resolve(rootDir, '..', '..', 'dist', 'options'), 23 | sourcemap: isDev, 24 | minify: isProduction, 25 | reportCompressedSize: isProduction, 26 | rollupOptions: { 27 | external: ['chrome'], 28 | }, 29 | }, 30 | define: { 31 | 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /pages/popup/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <title>Popup</title> 6 | </head> 7 | 8 | <body> 9 | <div id="app-container"></div> 10 | <script type="module" src="./src/index.tsx"></script> 11 | </body> 12 | </html> 13 | -------------------------------------------------------------------------------- /pages/popup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/popup", 3 | "version": "0.0.1", 4 | "description": "chrome extension popup", 5 | "private": true, 6 | "sideEffects": true, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf ./dist && rimraf .turbo", 12 | "build": "pnpm run clean && tsc --noEmit && vite build", 13 | "build:watch": "cross-env __DEV__=true vite build -w --mode development", 14 | "dev": "pnpm build:watch", 15 | "lint": "eslint . --ext .ts,.tsx", 16 | "lint:fix": "pnpm lint --fix", 17 | "prettier": "prettier . --write", 18 | "type-check": "tsc --noEmit" 19 | }, 20 | "dependencies": { 21 | "@sync-your-cookie/shared": "workspace:*", 22 | "@sync-your-cookie/storage": "workspace:*", 23 | "clsx": "^2.1.1", 24 | "lucide-react": "^0.394.0", 25 | "pako": "^2.1.0", 26 | "sonner": "^1.5.0", 27 | "tailwind-merge": "^2.3.0" 28 | }, 29 | "devDependencies": { 30 | "@sync-your-cookie/hmr": "workspace:*", 31 | "@sync-your-cookie/protobuf": "workspace:*", 32 | "@sync-your-cookie/tailwindcss-config": "workspace:*", 33 | "@sync-your-cookie/tsconfig": "workspace:*", 34 | "@sync-your-cookie/ui": "workspace:*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/popup/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | // 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /pages/popup/public/github.svg: -------------------------------------------------------------------------------- 1 | <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg> -------------------------------------------------------------------------------- /pages/popup/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/pages/popup/public/logo.png -------------------------------------------------------------------------------- /pages/popup/src/Popup.tsx: -------------------------------------------------------------------------------- 1 | import { extractDomainAndPort, useTheme, withErrorBoundary, withSuspense } from '@sync-your-cookie/shared'; 2 | 3 | import { Button, Image, Spinner, Toaster } from '@sync-your-cookie/ui'; 4 | import { CloudDownload, CloudUpload, Copyright, PanelRightOpen, RotateCw, Settings } from 'lucide-react'; 5 | import { useEffect, useState } from 'react'; 6 | 7 | import { AutoSwitch } from './components/AutoSwtich'; 8 | import { useDomainConfig } from './hooks/useDomainConfig'; 9 | 10 | const Popup = () => { 11 | const { theme } = useTheme(); 12 | const [activeTabUrl, setActiveTabUrl] = useState(''); 13 | const [favIconUrl, setFavIconUrl] = useState(''); 14 | 15 | const { 16 | pushing, 17 | toggleAutoPushState, 18 | toggleAutoPullState, 19 | domain, 20 | setDomain, 21 | domainItemConfig, 22 | domainItemStatus, 23 | handlePush, 24 | handlePull, 25 | } = useDomainConfig(); 26 | 27 | useEffect(() => { 28 | chrome.tabs.query({ active: true, lastFocusedWindow: true }, async function (tabs) { 29 | if (tabs.length > 0) { 30 | const activeTab = tabs[0]; 31 | if (activeTab.url && activeTab.url.startsWith('http')) { 32 | setFavIconUrl(activeTab?.favIconUrl || ''); 33 | setActiveTabUrl(activeTab.url); 34 | if (activeTab.url.includes('www.') && !1) { 35 | const urlObj = new URL(activeTab.url); 36 | setDomain(urlObj.hostname + `${urlObj.port ? ':' + urlObj.port : ''}`); 37 | } else { 38 | const [domain, tempPort] = await extractDomainAndPort(activeTab.url); 39 | setDomain(domain + `${tempPort ? ':' + tempPort : ''}`); 40 | } 41 | } 42 | } 43 | }); 44 | }, []); 45 | 46 | const isPushingOrPulling = domainItemStatus.pushing || domainItemStatus.pulling; 47 | 48 | return ( 49 | <div className="flex flex-col items-center min-w-[400px] justify-center bg-background "> 50 | <header className=" p-2 flex w-full justify-between items-center bg-card/50 shadow-md border-b border-border "> 51 | <div className="flex items-center"> 52 | <img 53 | src={chrome.runtime.getURL('options/logo.png')} 54 | className="h-10 w-10 overflow-hidden object-contain " 55 | alt="logo" 56 | /> 57 | <h2 className="text-base text-foreground font-bold">SyncYourCookie</h2> 58 | </div> 59 | <Button 60 | variant="ghost" 61 | onClick={() => { 62 | chrome.runtime.openOptionsPage(); 63 | }} 64 | className="cursor-pointer text-sm mr-[-8px] "> 65 | <Settings size={20} /> 66 | </Button> 67 | </header> 68 | <main className="p-4 "> 69 | <Spinner show={false}> 70 | {domain ? ( 71 | <div className="flex justify-center items-center mb-2 "> 72 | <Image src={favIconUrl} /> 73 | <h3 className="text-center whitespace-nowrap text-xl text-primary font-bold">{domain}</h3> 74 | </div> 75 | ) : null} 76 | 77 | <div className=" flex flex-col"> 78 | {/* <Button title={cloudflareAccountId} className="mb-2" onClick={handleUpdateToken}> 79 | Update Token 80 | </Button> */} 81 | <div className="flex items-center mb-2 "> 82 | <Button 83 | disabled={!activeTabUrl || isPushingOrPulling || pushing} 84 | className=" mr-2 w-[160px] justify-start" 85 | onClick={() => handlePush(domain, activeTabUrl, favIconUrl)}> 86 | {domainItemStatus.pushing ? ( 87 | <RotateCw size={16} className="mr-2 animate-spin" /> 88 | ) : ( 89 | <CloudUpload size={16} className="mr-2" /> 90 | )} 91 | Push cookie 92 | </Button> 93 | <AutoSwitch 94 | disabled={!activeTabUrl} 95 | onChange={() => toggleAutoPushState(domain)} 96 | id="autoPush" 97 | value={!!domainItemConfig.autoPush} 98 | /> 99 | </div> 100 | 101 | <div className="flex items-center mb-2 "> 102 | <Button 103 | disabled={!activeTabUrl || isPushingOrPulling} 104 | className=" w-[160px] mr-2 justify-start" 105 | onClick={() => handlePull(activeTabUrl)}> 106 | {domainItemStatus?.pulling ? ( 107 | <RotateCw size={16} className="mr-2 animate-spin" /> 108 | ) : ( 109 | <CloudDownload size={16} className="mr-2" /> 110 | )} 111 | Pull cookie 112 | </Button> 113 | 114 | <AutoSwitch 115 | disabled={!activeTabUrl} 116 | onChange={() => toggleAutoPullState(domain)} 117 | id="autoPull" 118 | value={!!domainItemConfig.autoPull} 119 | /> 120 | </div> 121 | 122 | <Button 123 | className="mb-2 justify-start" 124 | onClick={async () => { 125 | chrome.windows.getCurrent(async currentWindow => { 126 | // const res = await chrome.sidePanel.getOptions({ 127 | // tabId: currentWindow.id, 128 | // }); 129 | chrome.sidePanel 130 | .open({ windowId: currentWindow.id! }) 131 | .then(() => { 132 | console.log('Side panel opened successfully'); 133 | }) 134 | .catch(error => { 135 | console.error('Error opening side panel:', error); 136 | }); 137 | }); 138 | }}> 139 | <PanelRightOpen size={16} className="mr-2" /> 140 | Open Manager 141 | </Button> 142 | </div> 143 | <Toaster 144 | theme={theme} 145 | closeButton 146 | toastOptions={{ 147 | duration: 1500, 148 | style: { 149 | // width: 'max-content', 150 | // margin: '0 auto', 151 | }, 152 | // className: 'w-[240px]', 153 | }} 154 | visibleToasts={1} 155 | richColors 156 | position="top-center" 157 | /> 158 | </Spinner> 159 | </main> 160 | <footer className="w-full text-center justify-center p-4 flex items-center border-t border-border/90 "> 161 | <span> 162 | <Copyright size={16} /> 163 | </span> 164 | <a 165 | className=" inline-flex items-center mx-1 text-sm underline " 166 | href="https://github.com/jackluson" 167 | target="_blank" 168 | rel="noopener noreferrer"> 169 | jackluson 170 | </a> 171 | <a href="https://github.com/jackluson/sync-your-cookie" target="_blank" rel="noopener noreferrer"> 172 | <img 173 | src={chrome.runtime.getURL('popup/github.svg')} 174 | className="h-4 w-4 overflow-hidden object-contain " 175 | alt="logo" 176 | /> 177 | </a> 178 | </footer> 179 | </div> 180 | ); 181 | }; 182 | 183 | export default withErrorBoundary(withSuspense(Popup, <div> Loading ... </div>), <div> Error Occur </div>); 184 | -------------------------------------------------------------------------------- /pages/popup/src/components/AutoSwtich/index.tsx: -------------------------------------------------------------------------------- 1 | import { Label, Switch } from '@sync-your-cookie/ui'; 2 | 3 | interface AutoSwitchProps { 4 | value: boolean; 5 | onChange: (value: boolean) => void; 6 | id: string; 7 | disabled?: boolean; 8 | } 9 | export function AutoSwitch(props: AutoSwitchProps) { 10 | const { value, onChange, id, disabled } = props; 11 | return ( 12 | <div className="flex items-center space-x-2"> 13 | <Switch disabled={disabled} onCheckedChange={onChange} checked={value} id={id} /> 14 | <Label htmlFor={id}>Auto</Label> 15 | </div> 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /pages/popup/src/hooks/useDomainConfig.ts: -------------------------------------------------------------------------------- 1 | import { useCookieAction } from '@sync-your-cookie/shared'; 2 | import { useState } from 'react'; 3 | import { toast } from 'sonner'; 4 | export const useDomainConfig = () => { 5 | const [domain, setDomain] = useState(''); 6 | const cookieAction = useCookieAction(domain, toast); 7 | 8 | return { 9 | domain, 10 | setDomain, 11 | ...cookieAction, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /pages/popup/src/index.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 5 | 'Droid Sans', 'Helvetica Neue', sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | 9 | position: relative; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 14 | } 15 | -------------------------------------------------------------------------------- /pages/popup/src/index.tsx: -------------------------------------------------------------------------------- 1 | import Popup from '@src/Popup'; 2 | import '@src/index.css'; 3 | import { ThemeProvider } from '@sync-your-cookie/shared'; 4 | import '@sync-your-cookie/ui/css'; 5 | import { createRoot } from 'react-dom/client'; 6 | 7 | function init() { 8 | const appContainer = document.querySelector('#app-container'); 9 | if (!appContainer) { 10 | throw new Error('Can not find #app-container'); 11 | } 12 | const root = createRoot(appContainer); 13 | 14 | root.render( 15 | <ThemeProvider> 16 | <Popup /> 17 | </ThemeProvider>, 18 | ); 19 | } 20 | 21 | init(); 22 | -------------------------------------------------------------------------------- /pages/popup/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /pages/popup/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const uiConfig = require('@sync-your-cookie/ui/tailwind.config'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | ...uiConfig, 6 | }; 7 | -------------------------------------------------------------------------------- /pages/popup/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@src/*": ["src/*"] 7 | }, 8 | "jsx": "react-jsx", 9 | "types": ["chrome"] 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /pages/popup/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import { resolve } from 'path'; 4 | import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; 5 | 6 | const rootDir = resolve(__dirname); 7 | const srcDir = resolve(rootDir, 'src'); 8 | 9 | const isDev = process.env.__DEV__ === 'true'; 10 | const isProduction = !isDev; 11 | 12 | export default defineConfig({ 13 | resolve: { 14 | alias: { 15 | '@src': srcDir, 16 | }, 17 | }, 18 | base: '', 19 | plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], 20 | publicDir: resolve(rootDir, 'public'), 21 | build: { 22 | outDir: resolve(rootDir, '..', '..', 'dist', 'popup'), 23 | sourcemap: isDev, 24 | minify: isProduction, 25 | reportCompressedSize: isProduction, 26 | rollupOptions: { 27 | external: ['chrome'], 28 | }, 29 | }, 30 | define: { 31 | 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /pages/sidepanel/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <title>SidePanel</title> 6 | </head> 7 | 8 | <body> 9 | <div id="app-container"></div> 10 | <script type="module" src="./src/index.tsx"></script> 11 | </body> 12 | </html> 13 | -------------------------------------------------------------------------------- /pages/sidepanel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sync-your-cookie/sidepanel", 3 | "version": "0.0.1", 4 | "description": "chrome extension sidepanel", 5 | "private": true, 6 | "sideEffects": true, 7 | "files": [ 8 | "dist/**" 9 | ], 10 | "scripts": { 11 | "clean": "rimraf ./dist && rimraf .turbo", 12 | "build": "pnpm run clean && tsc --noEmit && vite build", 13 | "build:watch": "cross-env __DEV__=true vite build -w --mode development", 14 | "dev": "pnpm build:watch", 15 | "lint": "eslint . --ext .ts,.tsx", 16 | "lint:fix": "pnpm lint --fix", 17 | "prettier": "prettier . --write", 18 | "type-check": "tsc --noEmit" 19 | }, 20 | "dependencies": { 21 | "@sync-your-cookie/shared": "workspace:*", 22 | "@sync-your-cookie/storage": "workspace:*", 23 | "clsx": "^2.1.1", 24 | "lucide-react": "^0.394.0", 25 | "pako": "^2.1.0", 26 | "sonner": "^1.5.0", 27 | "tailwind-merge": "^2.3.0" 28 | }, 29 | "devDependencies": { 30 | "@sync-your-cookie/protobuf": "workspace:*", 31 | "@sync-your-cookie/tailwindcss-config": "workspace:*", 32 | "@sync-your-cookie/tsconfig": "workspace:*", 33 | "@sync-your-cookie/hmr": "workspace:*", 34 | "@sync-your-cookie/ui": "workspace:*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pages/sidepanel/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /pages/sidepanel/public/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> 2 | <g fill="#61DAFB"> 3 | <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> 4 | <circle cx="420.9" cy="296.5" r="45.7"/> 5 | <path d="M520.5 78.1z"/> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /pages/sidepanel/src/SidePanel.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme, withErrorBoundary, withSuspense } from '@sync-your-cookie/shared'; 2 | import { Toaster } from '@sync-your-cookie/ui'; 3 | import { useEffect } from 'react'; 4 | import CookieTable from './components/CookieTable'; 5 | const SidePanel = () => { 6 | useEffect(() => { 7 | chrome.runtime.onMessage.addListener(message => { 8 | // Might not be as easy if there are multiple side panels open 9 | if (message === 'closeSidePanel') { 10 | window.close(); 11 | } 12 | }); 13 | }, []); 14 | const { theme } = useTheme(); 15 | 16 | return ( 17 | <div className=""> 18 | <header></header> 19 | <CookieTable /> 20 | <Toaster 21 | theme={theme} 22 | closeButton 23 | toastOptions={{ 24 | duration: 1500, 25 | style: { 26 | // width: 'max-content', 27 | // margin: '0 auto', 28 | }, 29 | // className: 'w-[240px]', 30 | }} 31 | visibleToasts={1} 32 | richColors 33 | position="top-center" 34 | /> 35 | </div> 36 | ); 37 | }; 38 | 39 | export default withErrorBoundary(withSuspense(SidePanel, <div> Loading ... </div>), <div> Error Occur </div>); 40 | -------------------------------------------------------------------------------- /pages/sidepanel/src/components/CookieTable/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from '@sync-your-cookie/ui'; 2 | import { Search, X } from 'lucide-react'; 3 | import { FC, useState } from 'react'; 4 | 5 | export interface SearchInputProps { 6 | onEnter?: (val: string) => void; 7 | } 8 | 9 | export const SearchInput: FC<SearchInputProps> = props => { 10 | const { onEnter } = props; 11 | const [searchVal, setSearchVal] = useState(''); 12 | return ( 13 | <div className="flex relative "> 14 | <Search size={18} className="absolute top-[11px] left-[10px]" /> 15 | <Input 16 | value={searchVal} 17 | onChange={evt => { 18 | setSearchVal(evt.target.value); 19 | }} 20 | onBlur={() => { 21 | onEnter?.(searchVal.trim()); 22 | }} 23 | onKeyDown={evt => { 24 | if (evt.key === 'Enter' || evt.code === 'Enter') { 25 | onEnter?.(searchVal.trim()); 26 | } 27 | }} 28 | className="bg-gray-100 pl-[36px]" 29 | placeholder="Filter" 30 | /> 31 | {searchVal && ( 32 | <X 33 | onClick={() => { 34 | setSearchVal(''); 35 | onEnter?.(''); 36 | }} 37 | size={16} 38 | className="absolute top-[13px] right-[10px] cursor-pointer" 39 | /> 40 | )} 41 | </div> 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /pages/sidepanel/src/components/CookieTable/hooks/useAction.ts: -------------------------------------------------------------------------------- 1 | import { useCookieAction } from '@sync-your-cookie/shared'; 2 | import type { Cookie } from '@sync-your-cookie/storage/lib/cookieStorage'; 3 | import { useEffect, useState } from 'react'; 4 | import { toast } from 'sonner'; 5 | import { CookieItem } from './../index'; 6 | import { useSelected } from './useSelected'; 7 | 8 | export const useAction = (cookie: Cookie) => { 9 | const [loading, setLoading] = useState(false); 10 | const [currentSearchStr, setCurrentSearchStr] = useState(''); 11 | const { 12 | loading: loadingWithSelected, 13 | selectedDomain, 14 | showCookiesColumns, 15 | setSelectedDomain, 16 | cookieList, 17 | renderKeyValue, 18 | } = useSelected(cookie, currentSearchStr); 19 | 20 | useEffect(() => { 21 | setCurrentSearchStr(''); 22 | }, [selectedDomain]); 23 | 24 | const cookieAction = useCookieAction(selectedDomain, toast); 25 | const handleDelete = async (cookie: CookieItem) => { 26 | try { 27 | setLoading(true); 28 | await cookieAction.handleRemove(cookie.host); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | const handlePull = async (activeTabUrl: string, cookie: CookieItem) => { 35 | try { 36 | setLoading(true); 37 | await cookieAction.handlePull(activeTabUrl, cookie.host, false); 38 | } finally { 39 | setLoading(false); 40 | } 41 | }; 42 | 43 | const handlePush = async (cookie: CookieItem) => { 44 | try { 45 | setLoading(true); 46 | await cookieAction.handlePush(cookie.host); 47 | } finally { 48 | setLoading(false); 49 | } 50 | }; 51 | 52 | const handleViewCookies = async (domain: string) => { 53 | setSelectedDomain(domain); 54 | }; 55 | 56 | const handleBack = () => { 57 | setCurrentSearchStr(''); 58 | setSelectedDomain(''); 59 | }; 60 | 61 | const handleCopy = (domain: string, isJSON: boolean = false) => { 62 | const cookies = cookie.domainCookieMap?.[domain]?.cookies || []; 63 | if (cookies.length === 0) { 64 | toast.warning('no cookie to copy, check again.'); 65 | return; 66 | } 67 | if (!navigator.clipboard) { 68 | toast.warning('please check clipboard permission settings before copy '); 69 | return; 70 | } 71 | let copyText = ''; 72 | if (isJSON) { 73 | copyText = JSON.stringify(cookies, undefined, 2); 74 | } else { 75 | const pairs = []; 76 | for (const ck of cookies) { 77 | if (ck.value) { 78 | const pair = `${ck.name}=${ck.value}`; 79 | pairs.push(pair); 80 | } 81 | } 82 | copyText = pairs.join('; '); 83 | } 84 | navigator?.clipboard?.writeText(copyText).then( 85 | () => { 86 | toast.success('Copy success'); 87 | }, 88 | err => { 89 | console.log('err', err); 90 | toast.error('Copy failed'); 91 | }, 92 | ); 93 | }; 94 | 95 | const handleSearch = (val: string) => { 96 | setCurrentSearchStr(val); 97 | }; 98 | 99 | return { 100 | handleDelete, 101 | handlePull, 102 | handlePush, 103 | handleViewCookies, 104 | loading: loading || loadingWithSelected, 105 | selectedDomain, 106 | setSelectedDomain, 107 | handleBack, 108 | showCookiesColumns, 109 | cookieAction, 110 | handleCopy, 111 | currentSearchStr, 112 | // handlePush, 113 | handleSearch, 114 | renderKeyValue, 115 | cookieList: cookieList.filter(item => { 116 | if (currentSearchStr.trim()) { 117 | return ( 118 | item.domain.includes(currentSearchStr) || 119 | item.name.includes(currentSearchStr) || 120 | item.value.includes(currentSearchStr) 121 | ); 122 | } 123 | return true; 124 | }), 125 | }; 126 | }; 127 | -------------------------------------------------------------------------------- /pages/sidepanel/src/components/CookieTable/hooks/useCookieItem.ts: -------------------------------------------------------------------------------- 1 | import { 2 | catchHandler, 3 | editCookieItemUsingMessage, 4 | ICookie, 5 | removeCookieItemUsingMessage, 6 | } from '@sync-your-cookie/shared'; 7 | import { useState } from 'react'; 8 | import { toast } from 'sonner'; 9 | 10 | export const useCookieItem = (selectedDomain: string) => { 11 | const [loading, setLoading] = useState(false); 12 | 13 | const handleDeleteItem = async (id: string) => { 14 | try { 15 | setLoading(true); 16 | await removeCookieItemUsingMessage({ 17 | domain: selectedDomain, 18 | id, 19 | }) 20 | .then(async res => { 21 | if (res.isOk) { 22 | toast.success(res.msg || 'success'); 23 | } else { 24 | toast.error(res.msg || 'Deleted fail'); 25 | } 26 | }) 27 | .catch(err => { 28 | catchHandler(err, 'delete', toast); 29 | }); 30 | } finally { 31 | setLoading(false); 32 | } 33 | }; 34 | 35 | const handleEditItem = async (oldItem: ICookie, newItem: ICookie) => { 36 | try { 37 | setLoading(true); 38 | await editCookieItemUsingMessage({ 39 | domain: selectedDomain, 40 | oldItem, 41 | newItem, 42 | }) 43 | .then(async res => { 44 | if (res.isOk) { 45 | toast.success(res.msg || 'success'); 46 | } else { 47 | toast.error(res.msg || 'Edited fail'); 48 | return Promise.reject(res); 49 | } 50 | }) 51 | .catch(err => { 52 | catchHandler(err, 'edit', toast); 53 | return Promise.reject(err); 54 | }); 55 | } finally { 56 | setLoading(false); 57 | } 58 | }; 59 | return { 60 | loading, 61 | handleDeleteItem, 62 | handleEditItem, 63 | }; 64 | }; 65 | -------------------------------------------------------------------------------- /pages/sidepanel/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /pages/sidepanel/src/index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <title>Options</title> 6 | </head> 7 | 8 | <body> 9 | <div id="app-container"></div> 10 | <script type="module" src="./index.tsx"></script> 11 | </body> 12 | </html> 13 | -------------------------------------------------------------------------------- /pages/sidepanel/src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@src/index.css'; 2 | import SidePanel from '@src/SidePanel'; 3 | import { ThemeProvider } from '@sync-your-cookie/shared'; 4 | import '@sync-your-cookie/ui/css'; 5 | import { createRoot } from 'react-dom/client'; 6 | 7 | function init() { 8 | const appContainer = document.querySelector('#app-container'); 9 | if (!appContainer) { 10 | throw new Error('Can not find #app-container'); 11 | } 12 | const root = createRoot(appContainer); 13 | root.render( 14 | <ThemeProvider> 15 | <SidePanel /> 16 | </ThemeProvider>, 17 | ); 18 | } 19 | 20 | init(); 21 | -------------------------------------------------------------------------------- /pages/sidepanel/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const uiConfig = require('@sync-your-cookie/ui/tailwind.config'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | ...uiConfig, 6 | }; 7 | -------------------------------------------------------------------------------- /pages/sidepanel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sync-your-cookie/tsconfig/base", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@src/*": ["src/*"] 7 | }, 8 | "jsx": "react-jsx", 9 | "types": ["chrome"] 10 | }, 11 | "include": ["src"] 12 | } 13 | -------------------------------------------------------------------------------- /pages/sidepanel/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | import { resolve } from 'path'; 4 | import { watchRebuildPlugin } from '@sync-your-cookie/hmr'; 5 | 6 | const rootDir = resolve(__dirname); 7 | const srcDir = resolve(rootDir, 'src'); 8 | 9 | const isDev = process.env.__DEV__ === 'true'; 10 | const isProduction = !isDev; 11 | 12 | export default defineConfig({ 13 | resolve: { 14 | alias: { 15 | '@src': srcDir, 16 | }, 17 | }, 18 | base: '', 19 | plugins: [react(), isDev && watchRebuildPlugin({ refresh: true })], 20 | publicDir: resolve(rootDir, 'public'), 21 | build: { 22 | outDir: resolve(rootDir, '..', '..', 'dist', 'sidepanel'), 23 | sourcemap: isDev, 24 | minify: isProduction, 25 | reportCompressedSize: isProduction, 26 | rollupOptions: { 27 | external: ['chrome'], 28 | }, 29 | }, 30 | define: { 31 | 'process.env.NODE_ENV': isDev ? `"development"` : `"production"`, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "chrome-extension" 3 | - "pages/*" 4 | - "packages/*" 5 | -------------------------------------------------------------------------------- /private-policy.md: -------------------------------------------------------------------------------- 1 | Privacy Policy 2 | 3 | `SYNC COOKIE` does not collect any personal information. 4 | 5 | `SYNC COOKIE` doesn't embed any kind of analytics in the code 6 | 7 | `SYNC COOKIE` does not track its users in any way possible 8 | 9 | `SYNC COOKIE` stores your cookie data in your browser and, after you actively configure your cloudflare account settings, transmits the cookie-encoded data to your cloudflare account according to your settings. 10 | 11 | 12 | link: [Privacy Policy](https://www.freeprivacypolicy.com/live/0744ab35-18ca-4e12-af35-524666eba493) 13 | -------------------------------------------------------------------------------- /screenshots/kv/account-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/account-id.png -------------------------------------------------------------------------------- /screenshots/kv/copy_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/copy_token.png -------------------------------------------------------------------------------- /screenshots/kv/create_namepace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/create_namepace.png -------------------------------------------------------------------------------- /screenshots/kv/create_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/create_token.png -------------------------------------------------------------------------------- /screenshots/kv/created_token_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/created_token_list.png -------------------------------------------------------------------------------- /screenshots/kv/custom_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/custom_token.png -------------------------------------------------------------------------------- /screenshots/kv/finish_create_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/finish_create_token.png -------------------------------------------------------------------------------- /screenshots/kv/input_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/input_name.png -------------------------------------------------------------------------------- /screenshots/kv/namespaceId.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/namespaceId.png -------------------------------------------------------------------------------- /screenshots/kv/paste.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/paste.png -------------------------------------------------------------------------------- /screenshots/kv/push_cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/push_cookie.png -------------------------------------------------------------------------------- /screenshots/kv/reload_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/reload_page.png -------------------------------------------------------------------------------- /screenshots/kv/setting-permission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/kv/setting-permission.png -------------------------------------------------------------------------------- /screenshots/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/overview.png -------------------------------------------------------------------------------- /screenshots/overview_1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/overview_1280x800.png -------------------------------------------------------------------------------- /screenshots/panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/panel.png -------------------------------------------------------------------------------- /screenshots/panel_1280 × 800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/panel_1280 × 800.png -------------------------------------------------------------------------------- /screenshots/panel_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/panel_item.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/settings.png -------------------------------------------------------------------------------- /screenshots/settings_1280 × 800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/settings_1280 × 800.png -------------------------------------------------------------------------------- /screenshots/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/sync.png -------------------------------------------------------------------------------- /screenshots/sync_1280 × 800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/sync_1280 × 800.png -------------------------------------------------------------------------------- /screenshots/sync_1400 × 560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/sync_1400 × 560.png -------------------------------------------------------------------------------- /screenshots/sync_440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jackluson/sync-your-cookie/39c01c8e041bb27ecb936334cf3e7573072a8ea9/screenshots/sync_440x280.png -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "dev": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", "build/**"], 8 | "persistent": true 9 | }, 10 | "build": { 11 | "dependsOn": ["^build"], 12 | "outputs": ["../../dist/**", "dist/**", "build/**"], 13 | "cache": false 14 | }, 15 | "type-check": { 16 | "cache": false 17 | }, 18 | "lint": { 19 | "cache": false 20 | }, 21 | "lint:fix": { 22 | "cache": false 23 | }, 24 | "prettier": { 25 | "cache": false 26 | }, 27 | "test": { 28 | "dependsOn": [ 29 | "^test", "^build" 30 | ], 31 | "cache": false 32 | }, 33 | "clean": { 34 | "cache": false 35 | } 36 | } 37 | } 38 | --------------------------------------------------------------------------------