├── .commitlintrc ├── .editorconfig ├── .github ├── FUNDING.yml ├── assets │ ├── ByteLaughs.webp │ ├── donation.webp │ └── ko-fi.webp ├── dependabot.yml └── workflows │ ├── github-page.yml │ └── pack-crx.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .postcssrc ├── .prettierrc ├── .stylelintrc ├── .tool-versions ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslint.config.js ├── index.html ├── manifest.config.ts ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── favicon.svg ├── icons │ ├── about.svg │ ├── blog.svg │ ├── close.svg │ ├── code.svg │ ├── github.svg │ ├── kofi.svg │ ├── logo128.png │ ├── logo16.png │ ├── logo32.png │ ├── logo48.png │ ├── logo64.png │ ├── settings.svg │ └── x.svg └── images │ └── wechat.webp ├── scripts └── build-manifest.ts ├── spiders ├── Pipfile ├── Pipfile.lock ├── README.md └── today.py ├── src ├── App.vue ├── components │ ├── ContentCard.vue │ ├── DateTime.vue │ ├── ModeSelector.vue │ ├── SeasonFood.vue │ ├── SettingsMenu.vue │ └── TodayEvent.vue ├── composables │ ├── useGetSeasonFoodData.ts │ ├── useGetTodayEventData.ts │ ├── useLatestUpdateApi.ts │ ├── useMode.ts │ ├── useMoment.ts │ ├── useNextHolidayApi.ts │ └── useTodayInfoApi.ts ├── constants │ └── mode.ts ├── data │ ├── season_food.json │ └── today_in_history.json ├── env.d.ts ├── helpers │ ├── counter.ts │ ├── date.ts │ ├── random.ts │ ├── storage.ts │ └── style.ts ├── main.ts ├── services │ ├── ApiService.ts │ ├── LatestUpdateService.ts │ ├── NextHolidayService.ts │ └── TodayInfoService.ts ├── types.ts ├── variables.scss └── views │ └── ContentView.vue ├── supabase ├── config.toml └── functions │ └── latest-update │ ├── .npmrc │ ├── deno.json │ └── index.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dukeluo 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: huanluo 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/assets/ByteLaughs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/.github/assets/ByteLaughs.webp -------------------------------------------------------------------------------- /.github/assets/donation.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/.github/assets/donation.webp -------------------------------------------------------------------------------- /.github/assets/ko-fi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/.github/assets/ko-fi.webp -------------------------------------------------------------------------------- /.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/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 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/workflows/github-page.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to github page 2 | on: 3 | release: 4 | types: [created] 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build-and-deploy: 12 | runs-on: ubuntu-latest 13 | env: 14 | VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} 15 | VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Build 20 | run: | 21 | npm ci 22 | npm run build 23 | - name: Deploy 24 | uses: JamesIves/github-pages-deploy-action@v4.3.3 25 | with: 26 | BRANCH: gh-pages 27 | FOLDER: dist 28 | -------------------------------------------------------------------------------- /.github/workflows/pack-crx.yml: -------------------------------------------------------------------------------- 1 | name: Pack .crx 2 | 3 | on: 4 | release: 5 | types: [created] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | pack: 13 | runs-on: ubuntu-latest 14 | env: 15 | VITE_SUPABASE_URL: ${{ secrets.VITE_SUPABASE_URL }} 16 | VITE_SUPABASE_ANON_KEY: ${{ secrets.VITE_SUPABASE_ANON_KEY }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | - name: Read package.json 21 | id: package-version 22 | uses: martinbeentjes/npm-get-version-action@main 23 | - name: Build 24 | run: | 25 | npm ci 26 | npm run build 27 | - name: Pack .zip 28 | uses: cardinalby/webext-buildtools-pack-extension-dir-action@v1 29 | with: 30 | extensionDir: 'dist' 31 | zipFilePath: 'wai-v${{ steps.package-version.outputs.current-version }}.zip' 32 | - name: Pack .crx 33 | uses: cardinalby/webext-buildtools-chrome-crx-action@v2 34 | with: 35 | zipFilePath: 'wai-v${{ steps.package-version.outputs.current-version }}.zip' 36 | crxFilePath: 'wai-v${{ steps.package-version.outputs.current-version }}.crx' 37 | privateKey: ${{ secrets.CRX_PRIVATE_KEY }} 38 | - name: Update GitHub Release 39 | uses: softprops/action-gh-release@v1 40 | with: 41 | tag_name: 'v${{ steps.package-version.outputs.current-version }}' 42 | generate_release_notes: false 43 | files: | 44 | wai-v${{ steps.package-version.outputs.current-version }}.zip 45 | wai-v${{ steps.package-version.outputs.current-version }}.crx 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | # Supabase 25 | .branches 26 | .temp 27 | 28 | # dotenvx 29 | .env.keys 30 | .env.local 31 | .env.*.local -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "${1}" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "postcss-preset-env": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "tabWidth": 2 7 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["stylelint-config-clean-order/error", "stylelint-config-recommended-vue/scss"] 3 | } 4 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | python 3.11.4 2 | nodejs 20.18.3 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vue.volar", 4 | "denoland.vscode-deno" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "denoland.vscode-deno" 4 | }, 5 | "deno.enablePaths": [ 6 | "supabase/functions" 7 | ], 8 | "deno.lint": true, 9 | "deno.unstable": [ 10 | "bare-node-builtins", 11 | "byonm", 12 | "sloppy-imports", 13 | "unsafe-proto", 14 | "webgpu", 15 | "broadcast-channel", 16 | "worker-options", 17 | "cron", 18 | "kv", 19 | "ffi", 20 | "fs", 21 | "http", 22 | "net" 23 | ], 24 | "python.analysis.autoImportCompletions": true, 25 | "python.analysis.typeCheckingMode": "basic", 26 | "stylelint.validate": [ 27 | "vue" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## [1.5.0](https://github.com/dukeluo/wai/compare/v1.4.2...v1.5.0) - 2025-03-08 4 | 5 | ### 新增 6 | 7 | - 增加侧边栏设置菜单 8 | 9 | ### 变化 10 | 11 | - 更新「历史上的今天」数据 12 | 13 | ## [1.4.2](https://github.com/dukeluo/wai/compare/v1.4.1...v1.4.2) - 2024-03-02 14 | 15 | ### 修复 16 | 17 | - 修复在 2.29 号因缺少内置「历史上的今天」数据而崩溃的问题 18 | 19 | ## [1.4.1](https://github.com/dukeluo/wai/compare/v1.4.0...v1.4.1) - 2023-09-10 20 | 21 | ### 修复 22 | 23 | - 修复在狭窄屏幕下内容不歪的问题 24 | 25 | ## [1.4.0](https://github.com/dukeluo/wai/compare/v1.3.0...v1.4.0) - 2023-08-19 26 | 27 | ### 新增 28 | 29 | - 更多的平台字体支持 30 | 31 | ### 变化 32 | 33 | - 使用全新方式实现内容布局,展示区域更加方正 34 | - 更新「历史上的今天」数据 35 | 36 | ### 修复 37 | 38 | - 修复特定角度倾斜出现滚动条的问题 39 | 40 | ## [1.3.0](https://github.com/dukeluo/wai/compare/v1.1.0...v1.3.0) - 2023-01-18 41 | 42 | ### 新增 43 | 44 | - 上架 Add-ons for Firefox,支持 Firefox 浏览器 45 | 46 | ### 变化 47 | 48 | - 更新「历史上的今天」数据 49 | 50 | ### 修复 51 | 52 | - 修复在线版模式选择功能在 Chrome 浏览器不可用问题 53 | 54 | ## [1.1.0](https://github.com/dukeluo/wai/compare/v1.0.0...v1.1.0) - 2022-10-5 55 | 56 | ### 新增 57 | 58 | - 三种模式[柔和模式/连续模式/全面模式]支持,强度自己选择 59 | 60 | ### 变化 61 | 62 | - 更新「历史上的今天」数据 63 | - 更新「历史上的今天」布局 64 | 65 | ### 修复 66 | 67 | - 修复内容时背景没有全面覆盖问题 68 | 69 | ## [1.0.0](https://github.com/dukeluo/wai/releases/tag/v1.0.0) - 2022-06-25 70 | 71 | ### 新增 72 | 73 | - 活动脖子,预防颈椎病 74 | - 休息日指南,快速知晓下一个休息日 75 | - 当季蔬果提示,健康饮食每一天 76 | - 历史上的今天,回顾历史长河 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | Chrome Web Store Version 11 | 14 | Chrome Web Store Rating 18 | 21 | Chrome Web Store Rating 25 | 28 | Firefox Add-ons Version 32 | 35 | Firefox Add-ons Version 39 | 42 | Firefox Add-ons Rating 46 | 49 | Follow Author on X 53 | GitHub 54 |

55 |

56 | Featured|HelloGitHub 57 |

58 | 59 | ## 歪脖子新标签页 60 | 61 | > 一款可以预防颈椎病的新标签页扩展。 62 | 63 | ### 安装 64 | 65 | - [chrome web store](https://chrome.google.com/webstore/detail/%E6%AD%AA%E8%84%96%E5%AD%90%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5/ackimleclkemolnfcfajficenpbnaiba) 66 | - [Add-ons for Firefox](https://addons.mozilla.org/en-US/firefox/addon/%E6%AD%AA%E8%84%96%E5%AD%90%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5/) 67 | - [离线安装包](https://github.com/dukeluo/wai/releases) 68 | - [在线体验](https://wai.shaiwang.life/) 69 | 70 | ### 功能 71 | 72 | - 活动脖子,预防颈椎病 73 | - 三种模式支持,强度自己选择 74 | - 休息日指南,快速知晓下一个休息日 75 | - 当季蔬果提示,健康饮食每一天 76 | - 历史上的今天,回顾历史长河 77 | 78 | #### 工作模式 79 | 80 | - 柔和模式:每次触发时内容小范围摆动,标题和主体不颠倒展示 81 | - 连续模式:标题和主体不颠倒展示,每隔 4s 自动左右摆动 82 | - 全面模式:每次触发时内容随机角度摆动,标题和主体颠倒展示,为插件默认模式 83 | 84 | ### 鸣谢 85 | 86 | 歪脖子新标签页的诞生离不开以下资源的帮助,特感谢以下资源的提供: 87 | 88 | - 本应用图标由 OpenClipart-Vectors Pixabay 上发布。 89 | - 休息日指南由[免费节假日 API ](https://timor.tech/api/holiday)强力驱动。 90 | - 当季蔬果的数据来自知乎「一年 12 个月对应的应季时令水果蔬菜有哪些?」中[果蔬百科的回答](https://www.zhihu.com/question/21026884/answer/243125996)。 91 | - 历史上的今天的数据来自维基百科[历史上的今天](https://zh.m.wikipedia.org/zh-cn/%E5%8E%86%E5%8F%B2%E4%B8%8A%E7%9A%84%E4%BB%8A%E5%A4%A9)。 92 | 93 | ### 支持作者 94 | 95 |
96 | 97 | 98 | 99 |
100 | 101 | ### 其他 102 | 103 | - [「歪脖子新标签页」讨论吐槽区](https://github.com/dukeluo/wai/discussions) 104 | - [「歪脖子新标签页」开发计划](https://github.com/users/dukeluo/projects/2) 105 | - [「歪脖子新标签页」 Issue 列表](https://github.com/dukeluo/wai/issues) 106 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import tseslint from 'typescript-eslint' 3 | import globals from 'globals' 4 | import vuePlugin from 'eslint-plugin-vue' 5 | import prettierPlugin from 'eslint-plugin-prettier' 6 | import checkFilePlugin from 'eslint-plugin-check-file' 7 | import prettierConfig from 'eslint-config-prettier/flat' 8 | 9 | export default tseslint.config( 10 | { 11 | ignores: ['node_modules/**', 'dist/**', '**/*.json'], 12 | }, 13 | js.configs.recommended, 14 | tseslint.configs.strict, 15 | tseslint.configs.stylistic, 16 | ...vuePlugin.configs['flat/recommended'], 17 | { 18 | files: ['src/**/*.{ts,vue}'], 19 | plugins: { 20 | '@typescript-eslint': tseslint.plugin, 21 | }, 22 | languageOptions: { 23 | globals: { 24 | ...globals.browser, 25 | }, 26 | parserOptions: { 27 | parser: tseslint.parser, 28 | project: './tsconfig.json', 29 | extraFileExtensions: ['.vue'], 30 | ecmaVersion: 'latest', 31 | sourceType: 'module', 32 | }, 33 | }, 34 | rules: { 35 | '@typescript-eslint/naming-convention': [ 36 | 'error', 37 | { 38 | selector: 'enum', 39 | format: ['PascalCase'], 40 | }, 41 | { 42 | selector: 'enumMember', 43 | format: ['PascalCase'], 44 | }, 45 | { 46 | selector: 'typeParameter', 47 | format: ['PascalCase'], 48 | prefix: ['T'], 49 | }, 50 | { 51 | selector: 'interface', 52 | format: ['PascalCase'], 53 | prefix: ['I'], 54 | }, 55 | ], 56 | }, 57 | }, 58 | { 59 | files: ['**/*.scss'], 60 | processor: 'check-file/eslint-processor-check-file', 61 | }, 62 | { 63 | files: ['src/**/*.*'], 64 | plugins: { 65 | prettier: prettierPlugin, 66 | 'check-file': checkFilePlugin, 67 | }, 68 | rules: { 69 | 'prettier/prettier': 'error', 70 | 'check-file/no-index': [ 71 | 'error', 72 | { 73 | ignoreMiddleExtensions: true, 74 | }, 75 | ], 76 | 'check-file/folder-naming-convention': [ 77 | 'error', 78 | { 79 | 'src/*/': 'CAMEL_CASE', 80 | }, 81 | ], 82 | 'check-file/filename-naming-convention': [ 83 | 'error', 84 | { 85 | 'src/components/*.vue': 'PASCAL_CASE', 86 | 'src/composables/*.ts': 'CAMEL_CASE', 87 | 'src/data/*.json': 'SNAKE_CASE', 88 | 'src/helpers/*.ts': 'CAMEL_CASE', 89 | 'src/services/*.ts': 'PASCAL_CASE', 90 | }, 91 | ], 92 | }, 93 | }, 94 | prettierConfig 95 | ) 96 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 新标签页 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /manifest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | manifest_version: 3, 3 | name: '歪脖子新标签页', 4 | description: '一款可以预防颈椎病的新标签页扩展', 5 | version: process.env.npm_package_version, 6 | author: 'Huan Luo (https://shaiwang.life)', 7 | homepage_url: 'https://github.com/dukeluo/wai', 8 | chrome_url_overrides: { 9 | newtab: 'index.html', 10 | }, 11 | icons: { 12 | '16': 'icons/logo16.png', 13 | '32': 'icons/logo32.png', 14 | '48': 'icons/logo48.png', 15 | '64': 'icons/logo64.png', 16 | '128': 'icons/logo128.png', 17 | }, 18 | permissions: ['storage'], 19 | browser_specific_settings: { 20 | gecko: { 21 | id: '{8ff02995-1ecd-4d77-9b1c-f4994f9ae70f}', 22 | }, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wai", 3 | "description": "A new tab web extension that can prevent cervical spondylosis", 4 | "version": "1.5.0", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/dukeluo/wai.git" 8 | }, 9 | "keywords": [ 10 | "web extension", 11 | "chrome extension", 12 | "firefox add-on", 13 | "new tab", 14 | "funny", 15 | "awesome", 16 | "cervical spondylosis" 17 | ], 18 | "author": "Huan Luo (https://shaiwang.life)", 19 | "funding": [ 20 | { 21 | "type": "ko_fi", 22 | "url": "https://ko-fi.com/huanluo" 23 | }, 24 | { 25 | "type": "github", 26 | "url": "https://github.com/sponsors/dukeluo" 27 | } 28 | ], 29 | "license": "MPL-2.0", 30 | "bugs": { 31 | "url": "https://github.com/dukeluo/wai/issues" 32 | }, 33 | "homepage": "https://github.com/dukeluo/wai", 34 | "type": "module", 35 | "scripts": { 36 | "prepare": "husky install", 37 | "dev": "vite", 38 | "build": "vue-tsc --noEmit && vite build", 39 | "watch": "vite build --watch", 40 | "preview": "vite preview", 41 | "lint": "eslint . && stylelint \"**/*.{vue, scss}\"", 42 | "lint:fix": "eslint . --fix && stylelint \"**/*.{vue, scss}\" --fix", 43 | "supabase:functions:deploy": "supabase functions deploy --project-ref lrymgxtzbdzqgoesfnan" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^17.0.3", 47 | "@commitlint/config-conventional": "^17.0.3", 48 | "@eslint/js": "^9.21.0", 49 | "@supabase/supabase-js": "^2.49.1", 50 | "@types/chrome": "0.0.197", 51 | "@types/node": "20.14.8", 52 | "@vitejs/plugin-vue": "5.2.1", 53 | "eslint": "^9.21.0", 54 | "eslint-config-prettier": "^10.1.1", 55 | "eslint-plugin-check-file": "^3.1.0", 56 | "eslint-plugin-prettier": "^5.2.3", 57 | "eslint-plugin-vue": "^9.32.0", 58 | "globals": "^15.0.0", 59 | "husky": "8.0.1", 60 | "postcss-html": "^1.5.0", 61 | "postcss-preset-env": "^9.1.1", 62 | "prettier": "^3.2.5", 63 | "sass": "1.53.0", 64 | "stylelint": "^16.15.0", 65 | "stylelint-config-clean-order": "^7.0.0", 66 | "stylelint-config-recommended-scss": "^14.1.0", 67 | "stylelint-config-recommended-vue": "^1.6.0", 68 | "supabase": "^2.15.8", 69 | "typescript": "^5.7.3", 70 | "typescript-eslint": "^8.26.0", 71 | "vite": "^6.2.0", 72 | "vite-plugin-banner": "^0.8.0", 73 | "vue": "3.5.13", 74 | "vue-tsc": "2.2.0" 75 | }, 76 | "engines": { 77 | "node": ">=20" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/favicon.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | Created with Fabric.js 3.5.0 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/icons/about.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/blog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/icons/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/icons/kofi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/icons/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/icons/logo128.png -------------------------------------------------------------------------------- /public/icons/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/icons/logo16.png -------------------------------------------------------------------------------- /public/icons/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/icons/logo32.png -------------------------------------------------------------------------------- /public/icons/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/icons/logo48.png -------------------------------------------------------------------------------- /public/icons/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/icons/logo64.png -------------------------------------------------------------------------------- /public/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /public/images/wechat.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dukeluo/wai/469c921d68837cec0b045cb6af7062b8c2e05b55/public/images/wechat.webp -------------------------------------------------------------------------------- /scripts/build-manifest.ts: -------------------------------------------------------------------------------- 1 | import manifest from '../manifest.config' 2 | import { writeFile } from 'fs/promises' 3 | import { resolve } from 'path' 4 | import type { Plugin } from 'vite' 5 | 6 | const FILE_NAME = 'manifest.json' 7 | const BUILD_PATH = 'dist' 8 | 9 | const buildManifest = async () => { 10 | const file = resolve(__dirname, '..', BUILD_PATH, FILE_NAME) 11 | 12 | await writeFile(file, JSON.stringify(manifest, undefined, 2)) 13 | console.info('✓ manifest.json is created.') 14 | } 15 | 16 | export const viteBuildManifest: () => Plugin = () => ({ 17 | name: 'vite-build-manifest', 18 | apply: 'build', 19 | enforce: 'pre', 20 | closeBundle: buildManifest, 21 | }) 22 | -------------------------------------------------------------------------------- /spiders/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | bs4 = "*" 9 | 10 | [dev-packages] 11 | pylint = "*" 12 | autopep8 = "*" 13 | 14 | [requires] 15 | python_version = "3" 16 | -------------------------------------------------------------------------------- /spiders/Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "de89200d610e9365514b8e720fd90f490c210a84d11f9b098242550b2be4f4aa" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "beautifulsoup4": { 20 | "hashes": [ 21 | "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da", 22 | "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a" 23 | ], 24 | "markers": "python_full_version >= '3.6.0'", 25 | "version": "==4.12.2" 26 | }, 27 | "bs4": { 28 | "hashes": [ 29 | "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a" 30 | ], 31 | "index": "pypi", 32 | "version": "==0.0.1" 33 | }, 34 | "certifi": { 35 | "hashes": [ 36 | "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", 37 | "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" 38 | ], 39 | "index": "pypi", 40 | "markers": "python_version >= '3.6'", 41 | "version": "==2024.7.4" 42 | }, 43 | "charset-normalizer": { 44 | "hashes": [ 45 | "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", 46 | "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", 47 | "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", 48 | "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", 49 | "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", 50 | "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", 51 | "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", 52 | "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", 53 | "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", 54 | "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", 55 | "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", 56 | "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", 57 | "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", 58 | "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", 59 | "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", 60 | "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", 61 | "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", 62 | "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", 63 | "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", 64 | "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", 65 | "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", 66 | "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", 67 | "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", 68 | "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", 69 | "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", 70 | "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", 71 | "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", 72 | "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", 73 | "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", 74 | "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", 75 | "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", 76 | "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", 77 | "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", 78 | "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", 79 | "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", 80 | "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", 81 | "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", 82 | "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", 83 | "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", 84 | "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", 85 | "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", 86 | "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", 87 | "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", 88 | "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", 89 | "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", 90 | "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", 91 | "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", 92 | "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", 93 | "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", 94 | "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", 95 | "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", 96 | "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", 97 | "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", 98 | "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", 99 | "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", 100 | "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", 101 | "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", 102 | "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", 103 | "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", 104 | "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", 105 | "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", 106 | "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", 107 | "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", 108 | "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", 109 | "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", 110 | "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", 111 | "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", 112 | "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", 113 | "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", 114 | "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", 115 | "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", 116 | "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", 117 | "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", 118 | "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", 119 | "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", 120 | "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", 121 | "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", 122 | "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", 123 | "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", 124 | "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", 125 | "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", 126 | "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", 127 | "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", 128 | "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", 129 | "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", 130 | "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", 131 | "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", 132 | "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", 133 | "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", 134 | "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" 135 | ], 136 | "markers": "python_full_version >= '3.7.0'", 137 | "version": "==3.3.2" 138 | }, 139 | "idna": { 140 | "hashes": [ 141 | "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", 142 | "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" 143 | ], 144 | "markers": "python_version >= '3.5'", 145 | "version": "==3.7" 146 | }, 147 | "requests": { 148 | "hashes": [ 149 | "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5", 150 | "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8" 151 | ], 152 | "index": "pypi", 153 | "markers": "python_version >= '3.8'", 154 | "version": "==2.32.0" 155 | }, 156 | "soupsieve": { 157 | "hashes": [ 158 | "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", 159 | "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" 160 | ], 161 | "markers": "python_version >= '3.8'", 162 | "version": "==2.5" 163 | }, 164 | "urllib3": { 165 | "hashes": [ 166 | "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", 167 | "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" 168 | ], 169 | "index": "pypi", 170 | "markers": "python_version >= '3.8'", 171 | "version": "==2.2.2" 172 | } 173 | }, 174 | "develop": { 175 | "astroid": { 176 | "hashes": [ 177 | "sha256:1aa149fc5c6589e3d0ece885b4491acd80af4f087baafa3fb5203b113e68cd3c", 178 | "sha256:6c107453dffee9055899705de3c9ead36e74119cee151e5a9aaf7f0b0e020a6a" 179 | ], 180 | "markers": "python_full_version >= '3.7.2'", 181 | "version": "==2.15.8" 182 | }, 183 | "autopep8": { 184 | "hashes": [ 185 | "sha256:86e9303b5e5c8160872b2f5ef611161b2893e9bfe8ccc7e2f76385947d57a2f1", 186 | "sha256:f9849cdd62108cb739dbcdbfb7fdcc9a30d1b63c4cc3e1c1f893b5360941b61c" 187 | ], 188 | "index": "pypi", 189 | "markers": "python_version >= '3.6'", 190 | "version": "==2.0.2" 191 | }, 192 | "dill": { 193 | "hashes": [ 194 | "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", 195 | "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" 196 | ], 197 | "markers": "python_version >= '3.11'", 198 | "version": "==0.3.7" 199 | }, 200 | "isort": { 201 | "hashes": [ 202 | "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", 203 | "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" 204 | ], 205 | "markers": "python_full_version >= '3.8.0'", 206 | "version": "==5.12.0" 207 | }, 208 | "lazy-object-proxy": { 209 | "hashes": [ 210 | "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382", 211 | "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82", 212 | "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9", 213 | "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494", 214 | "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46", 215 | "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30", 216 | "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63", 217 | "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4", 218 | "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae", 219 | "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be", 220 | "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701", 221 | "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd", 222 | "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006", 223 | "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a", 224 | "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586", 225 | "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8", 226 | "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821", 227 | "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07", 228 | "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b", 229 | "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171", 230 | "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b", 231 | "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2", 232 | "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7", 233 | "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4", 234 | "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8", 235 | "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e", 236 | "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f", 237 | "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda", 238 | "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4", 239 | "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e", 240 | "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671", 241 | "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11", 242 | "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455", 243 | "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734", 244 | "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb", 245 | "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59" 246 | ], 247 | "markers": "python_version >= '3.7'", 248 | "version": "==1.9.0" 249 | }, 250 | "mccabe": { 251 | "hashes": [ 252 | "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", 253 | "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" 254 | ], 255 | "markers": "python_version >= '3.6'", 256 | "version": "==0.7.0" 257 | }, 258 | "platformdirs": { 259 | "hashes": [ 260 | "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", 261 | "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" 262 | ], 263 | "markers": "python_version >= '3.7'", 264 | "version": "==3.11.0" 265 | }, 266 | "pycodestyle": { 267 | "hashes": [ 268 | "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", 269 | "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" 270 | ], 271 | "markers": "python_version >= '3.8'", 272 | "version": "==2.11.1" 273 | }, 274 | "pylint": { 275 | "hashes": [ 276 | "sha256:73995fb8216d3bed149c8d51bba25b2c52a8251a2c8ac846ec668ce38fab5413", 277 | "sha256:f7b601cbc06fef7e62a754e2b41294c2aa31f1cb659624b9a85bcba29eaf8252" 278 | ], 279 | "index": "pypi", 280 | "markers": "python_full_version >= '3.7.2'", 281 | "version": "==2.17.5" 282 | }, 283 | "tomlkit": { 284 | "hashes": [ 285 | "sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86", 286 | "sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899" 287 | ], 288 | "markers": "python_version >= '3.7'", 289 | "version": "==0.12.1" 290 | }, 291 | "wrapt": { 292 | "hashes": [ 293 | "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0", 294 | "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420", 295 | "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a", 296 | "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c", 297 | "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079", 298 | "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923", 299 | "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f", 300 | "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1", 301 | "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8", 302 | "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86", 303 | "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0", 304 | "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364", 305 | "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e", 306 | "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c", 307 | "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e", 308 | "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c", 309 | "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727", 310 | "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff", 311 | "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e", 312 | "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29", 313 | "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7", 314 | "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72", 315 | "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475", 316 | "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a", 317 | "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317", 318 | "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2", 319 | "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd", 320 | "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640", 321 | "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98", 322 | "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248", 323 | "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e", 324 | "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d", 325 | "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec", 326 | "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1", 327 | "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e", 328 | "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9", 329 | "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92", 330 | "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb", 331 | "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094", 332 | "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46", 333 | "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29", 334 | "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd", 335 | "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705", 336 | "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8", 337 | "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975", 338 | "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb", 339 | "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e", 340 | "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b", 341 | "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418", 342 | "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019", 343 | "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1", 344 | "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba", 345 | "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6", 346 | "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2", 347 | "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3", 348 | "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7", 349 | "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752", 350 | "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416", 351 | "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f", 352 | "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1", 353 | "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc", 354 | "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145", 355 | "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee", 356 | "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a", 357 | "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7", 358 | "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b", 359 | "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653", 360 | "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0", 361 | "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90", 362 | "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29", 363 | "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6", 364 | "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034", 365 | "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09", 366 | "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559", 367 | "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639" 368 | ], 369 | "markers": "python_version >= '3.11'", 370 | "version": "==1.15.0" 371 | } 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /spiders/README.md: -------------------------------------------------------------------------------- 1 | `today.py` 用于从维基百科爬取「历史上的今天」数据。 2 | 3 | ### 使用 4 | 5 | - `python -m ensurepip` 6 | - `pip install pipenv` 7 | - `pipenv install` 8 | - `pipenv run python -m today` 9 | 10 | ### 清除环境 11 | 12 | - `pipenv --venv` 13 | - `pipenv --rm` 14 | -------------------------------------------------------------------------------- /spiders/today.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf_8 -*- 2 | import datetime 3 | import re 4 | import json 5 | import requests 6 | from bs4 import BeautifulSoup 7 | 8 | EVENT_MAX_LENGTH = 64 9 | 10 | 11 | def getCurrentYear(): 12 | return datetime.datetime.now().year 13 | 14 | 15 | def getDateList(): 16 | year = getCurrentYear() 17 | date = datetime.date(year, 1, 1) 18 | list = [] 19 | 20 | for i in range(366): 21 | date_str = str(date.month) + "月" + str(date.day) + "日" 22 | list.append(date_str) 23 | date += datetime.timedelta(days=1) 24 | 25 | return list 26 | 27 | 28 | def getEventText(year, event): 29 | text = "%s:%s" % (year, event) 30 | text = re.sub(r"\[\S*?\]", "", text) # 去除引用标记 31 | text = text.strip("。;") # 去除末尾符号 32 | 33 | return text 34 | 35 | 36 | def parseLiItem(liItem): 37 | text = liItem.get_text().strip() 38 | match = re.compile("((^前|^)\d{1,4}年):([\s\S]*$)").match(text) 39 | list = [] 40 | 41 | if match: 42 | year = match.group(1) 43 | events = filter(lambda x: len(x) < EVENT_MAX_LENGTH, 44 | match.group(3).strip().split("\n")) 45 | list = map(lambda x: getEventText(year, x), events) 46 | 47 | return list 48 | 49 | 50 | def getDayEvents(html): 51 | # Find the main 大事记 section 52 | main_section_pattern = re.compile( 53 | r'

.*?

.*?
([\s\S]*?)
') 54 | main_match = main_section_pattern.search(html) 55 | 56 | list = [] 57 | 58 | if main_match: 59 | # Get all content between 大事记 and the next main heading 60 | content = main_match.group(1) 61 | 62 | # Process all
  • elements within this section (across all subsections) 63 | bsObj = BeautifulSoup(content, "html.parser").findAll("li") 64 | for li in bsObj: 65 | list.extend(parseLiItem(li)) 66 | else: 67 | print("未找到大事记部分") 68 | 69 | print("共有 %s 条" % list.__len__()) 70 | 71 | return list 72 | 73 | 74 | def main(): 75 | dateList = getDateList() 76 | data = {} 77 | 78 | for date in dateList: 79 | print("正在获取 %s 的数据..." % date) 80 | r = requests.get("https://zh.wikipedia.org/wiki/%s" % 81 | date, headers={'Accept-Language': 'en-US,zh-CN;q=0.5'}) 82 | events = getDayEvents(r.text) 83 | dd = datetime.datetime.strptime(date, "%m月%d日") 84 | 85 | if (data.get(dd.month) == None): 86 | data[dd.month] = {} 87 | data[dd.month][dd.day] = events 88 | 89 | with open("../src/data/today_in_history.json", "w", encoding="utf-8") as f: 90 | json.dump(data, f, ensure_ascii=False, indent=2) 91 | 92 | 93 | if __name__ == "__main__": 94 | main() 95 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 110 | -------------------------------------------------------------------------------- /src/components/ContentCard.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 35 | 36 | 90 | -------------------------------------------------------------------------------- /src/components/DateTime.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 29 | 30 | 54 | -------------------------------------------------------------------------------- /src/components/ModeSelector.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | 49 | 110 | -------------------------------------------------------------------------------- /src/components/SeasonFood.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/components/SettingsMenu.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 88 | 89 | 243 | -------------------------------------------------------------------------------- /src/components/TodayEvent.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /src/composables/useGetSeasonFoodData.ts: -------------------------------------------------------------------------------- 1 | import { Ref, onMounted, ref } from 'vue' 2 | import ContentCard from '../components/ContentCard.vue' 3 | import _seasonFoodData from '../data/season_food.json' 4 | import { getMonth } from '../helpers/date' 5 | import { isParagraphMultipleLinesInW3 } from '../helpers/style' 6 | import type { Month, SeasonFood } from '../types' 7 | 8 | const seasonFoodData = _seasonFoodData as SeasonFood 9 | 10 | const getRandomFoods = (container: HTMLElement, foods: string[]) => { 11 | const start = Math.floor(Math.random() * foods.length) 12 | const items: string[] = [] 13 | let index = start 14 | 15 | while (true) { 16 | const food = foods[index] 17 | 18 | items.push(food) 19 | if (isParagraphMultipleLinesInW3(container, items.join('、'))) { 20 | items.pop() 21 | if (index === start) break 22 | } 23 | index = (index + 1) % foods.length 24 | } 25 | 26 | return items 27 | } 28 | 29 | export const useGetSeasonFoodData = (date: Date, cardRef: Ref | undefined>) => { 30 | const month = getMonth(date).toString() as Month 31 | const { vegetables, fruits } = seasonFoodData[month] 32 | const data = ref([]) 33 | 34 | onMounted(() => { 35 | const content = cardRef.value?.contentRef 36 | 37 | if (!content) return 38 | 39 | data.value = [getRandomFoods(content, vegetables).join('、'), getRandomFoods(content, fruits).join('、')] 40 | }) 41 | 42 | return data 43 | } 44 | -------------------------------------------------------------------------------- /src/composables/useGetTodayEventData.ts: -------------------------------------------------------------------------------- 1 | import { Ref, onMounted, ref } from 'vue' 2 | import _todayInHistoryData from '../data/today_in_history.json' 3 | import { getDay, getMonth } from '../helpers/date' 4 | import type { Day, Month, YearInHistory } from '../types' 5 | import { measureParagraph } from '../helpers/style' 6 | import ContentCard from '../components/ContentCard.vue' 7 | 8 | const todayInHistoryData = _todayInHistoryData as YearInHistory 9 | 10 | export const useGetTodayEventData = (date: Date, cardRef: Ref | undefined>) => { 11 | const month = getMonth(date).toString() as Month 12 | const day = getDay(date).toString() as Day 13 | const todayHistoryEvents = todayInHistoryData[month][day] ?? [] 14 | const events = ref([]) 15 | 16 | onMounted(() => { 17 | const container = cardRef.value?.containerRef 18 | const title = cardRef.value?.titleRef 19 | const content = cardRef.value?.contentRef 20 | 21 | if (!container || !content || !title) return 22 | 23 | const { marginTop: titleMarginTop, marginBottom: titleMarginBottom } = window.getComputedStyle(title) 24 | const titleHeight = Math.round(title.offsetHeight + parseFloat(titleMarginTop) + parseFloat(titleMarginBottom)) 25 | const maxHeight = container.offsetHeight - titleHeight 26 | const start = Math.floor(Math.random() * todayHistoryEvents.length) 27 | const items: string[] = [] 28 | let currentHeight = content.offsetHeight 29 | let index = start 30 | 31 | while (true) { 32 | const event = todayHistoryEvents[index] 33 | const { height } = measureParagraph(content, event) 34 | 35 | currentHeight += height 36 | items.push(event) 37 | if (currentHeight > maxHeight) { 38 | items.pop() 39 | if (index === start) break 40 | currentHeight -= height 41 | } 42 | index = (index + 1) % todayHistoryEvents.length 43 | } 44 | events.value = items 45 | }) 46 | 47 | return { events } 48 | } 49 | -------------------------------------------------------------------------------- /src/composables/useLatestUpdateApi.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from 'vue' 2 | import { LatestUpdateService } from '../services/LatestUpdateService' 3 | import { ILatestUpdate } from '../types' 4 | 5 | export function useLatestUpdate() { 6 | const service = new LatestUpdateService() 7 | const latestUpdate = ref(null) 8 | const isLoading = ref(false) 9 | const error = ref('') 10 | 11 | const fetchAboutContent = async () => { 12 | try { 13 | isLoading.value = true 14 | error.value = '' 15 | 16 | const data = await service.fetch() 17 | latestUpdate.value = data 18 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 19 | } catch (err) { 20 | error.value = 'Failed to load latest update' 21 | } finally { 22 | isLoading.value = false 23 | } 24 | } 25 | 26 | onMounted(() => { 27 | fetchAboutContent() 28 | }) 29 | 30 | return { 31 | latestUpdate, 32 | isLoading, 33 | error, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/composables/useMode.ts: -------------------------------------------------------------------------------- 1 | import { computed, onBeforeMount, onBeforeUnmount, reactive, ref, watch } from 'vue' 2 | import { MODE_CONFIG } from '../constants/mode' 3 | import { storage } from '../helpers/storage' 4 | import type { IModeConfig, IModeConfigValue } from '../types' 5 | import { Mode } from '../types' 6 | 7 | const MODE_KEY = 'setting.mode' 8 | 9 | const getConfigValue = ({ turn, isReversed, interval }: IModeConfig): IModeConfigValue => ({ 10 | turn: turn(), 11 | isReversed, 12 | interval, 13 | }) 14 | 15 | export const useMode = () => { 16 | const mode = ref(Mode.Full) 17 | const config = computed(() => MODE_CONFIG[mode.value]) 18 | const value = reactive(getConfigValue(config.value)) 19 | let timer: NodeJS.Timeout 20 | 21 | const update = () => { 22 | const newValue = getConfigValue(config.value) 23 | 24 | value.turn = newValue.turn 25 | value.isReversed = newValue.isReversed 26 | value.interval = newValue.interval 27 | 28 | return newValue 29 | } 30 | 31 | const clear = () => timer && clearTimeout(timer) 32 | 33 | watch(mode, (m) => { 34 | const newValue = update() 35 | 36 | clear() 37 | storage.setItem(MODE_KEY, m) 38 | 39 | if (newValue.interval > 0) { 40 | timer = setInterval(update, newValue.interval * 1000) 41 | } 42 | }) 43 | 44 | onBeforeMount(async () => { 45 | const settingMode = (await storage.getItem(MODE_KEY)) as Mode | null 46 | 47 | mode.value = settingMode ?? Mode.Full 48 | }) 49 | 50 | onBeforeUnmount(clear) 51 | 52 | return { mode, config: value } 53 | } 54 | -------------------------------------------------------------------------------- /src/composables/useMoment.ts: -------------------------------------------------------------------------------- 1 | import { ref, onBeforeUnmount } from 'vue' 2 | 3 | export const useMoment = () => { 4 | const moment = ref(new Date()) 5 | const update = () => (moment.value = new Date()) 6 | const timer = setInterval(update, 1000) 7 | 8 | onBeforeUnmount(() => { 9 | clearInterval(timer) 10 | }) 11 | 12 | return { 13 | moment, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/composables/useNextHolidayApi.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from 'vue' 2 | import { getDate } from '../helpers/date' 3 | import { NextHolidayService } from '../services/NextHolidayService' 4 | 5 | export const useNextHolidayApi = (time: Date) => { 6 | const service = new NextHolidayService(getDate(time)) 7 | const name = ref('--') 8 | const rest = ref('-') 9 | 10 | onMounted(async () => { 11 | const { code, holiday } = await service.fetch() 12 | 13 | if (code === 0) { 14 | name.value = holiday.name 15 | rest.value = holiday.rest 16 | } 17 | }) 18 | 19 | return { name, rest } 20 | } 21 | -------------------------------------------------------------------------------- /src/composables/useTodayInfoApi.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from 'vue' 2 | import { getDate } from '../helpers/date' 3 | import { TodayInfoService } from '../services/TodayInfoService' 4 | 5 | export const useTodayInfoApi = (time: Date) => { 6 | const service = new TodayInfoService(getDate(time)) 7 | const isDayOff = ref(false) 8 | 9 | onMounted(async () => { 10 | const { code, type: todayInfo } = await service.fetch() 11 | 12 | if (code === 0) { 13 | if (todayInfo.type === 1 || todayInfo.type === 2) { 14 | isDayOff.value = true 15 | } 16 | } 17 | }) 18 | 19 | return { isDayOff } 20 | } 21 | -------------------------------------------------------------------------------- /src/constants/mode.ts: -------------------------------------------------------------------------------- 1 | import { Mode } from '../types' 2 | import type { IModeConfig } from '../types' 3 | import { randomNumber } from '../helpers/random' 4 | import { counter } from '../helpers/counter' 5 | 6 | export const MODE_CONFIG: Record = { 7 | [Mode.Full]: { 8 | turn: () => randomNumber(0, 1), 9 | isReversed: true, 10 | interval: -1, 11 | }, 12 | [Mode.Continuous]: { 13 | turn: () => [0.15, 0.85][(counter.next().value ?? 0) % 2], 14 | isReversed: false, 15 | interval: 4, 16 | }, 17 | [Mode.Soft]: { 18 | turn: () => (Math.random() > 0.5 ? randomNumber(0.05, 0.15) : randomNumber(0.85, 1)), 19 | isReversed: false, 20 | interval: -1, 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /src/data/season_food.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "vegetables": [ 4 | "白菜", 5 | "菠菜", 6 | "彩椒", 7 | "胡萝卜", 8 | "小白菜", 9 | "芥菜", 10 | "平菇", 11 | "蒜黄", 12 | "香菇", 13 | "杏鲍菇", 14 | "紫菜", 15 | "豆瓣菜", 16 | "紫菜苔" 17 | ], 18 | "fruits": ["菠萝", "甘蔗", "砂糖橘", "蛋黄果", "番石榴", "橙子", "青枣"] 19 | }, 20 | "2": { 21 | "vegetables": [ 22 | "白菜", 23 | "彩椒", 24 | "春笋", 25 | "胡萝卜", 26 | "韭菜", 27 | "小白菜", 28 | "芥菜", 29 | "平菇", 30 | "蒜黄", 31 | "香菇", 32 | "紫菜", 33 | "豆瓣菜", 34 | "紫菜苔" 35 | ], 36 | "fruits": ["甘蔗", "柠檬", "圣女果", "蛋黄果", "番石榴", "橙子", "丑柑", "青枣"] 37 | }, 38 | "3": { 39 | "vegetables": [ 40 | "荸荠", 41 | "彩椒", 42 | "春笋", 43 | "胡萝卜", 44 | "韭菜", 45 | "小白菜", 46 | "芥菜", 47 | "芦笋", 48 | "平菇", 49 | "蒜黄", 50 | "茼蒿", 51 | "香菇", 52 | "香椿", 53 | "紫菜", 54 | "豆瓣菜", 55 | "紫菜苔" 56 | ], 57 | "fruits": ["柠檬", "枇杷", "圣女果", "番石榴", "橙子", "丑柑", "青枣", "青梅"] 58 | }, 59 | "4": { 60 | "vegetables": [ 61 | "菠菜", 62 | "荸荠", 63 | "彩椒", 64 | "春笋", 65 | "茶树菇", 66 | "大葱", 67 | "韭菜", 68 | "蒜苔", 69 | "西兰花", 70 | "黄豆芽", 71 | "荠菜", 72 | "空心菜", 73 | "芦笋", 74 | "绿豆芽", 75 | "平菇", 76 | "青萝卜", 77 | "蒜黄", 78 | "茼蒿", 79 | "莴苣", 80 | "香菇", 81 | "香椿", 82 | "蟹味菇", 83 | "紫菜", 84 | "快菜", 85 | "豌豆", 86 | "鸡腿菇", 87 | "草菇", 88 | "竹荪", 89 | "荷兰豆", 90 | "银耳" 91 | ], 92 | "fruits": ["菠萝", "火龙果", "枇杷", "圣女果", "丑柑", "青梅"] 93 | }, 94 | "5": { 95 | "vegetables": [ 96 | "白萝卜", 97 | "菠菜", 98 | "彩椒", 99 | "春笋", 100 | "茶树菇", 101 | "大葱", 102 | "韭菜", 103 | "蒜苔", 104 | "土豆", 105 | "西兰花", 106 | "圆白菜", 107 | "洋葱", 108 | "油菜", 109 | "茴香", 110 | "海带", 111 | "黄豆芽", 112 | "荠菜", 113 | "苦瓜", 114 | "空心菜", 115 | "绿豆芽", 116 | "平菇", 117 | "青萝卜", 118 | "生菜", 119 | "莴苣", 120 | "豌豆苗", 121 | "苋菜", 122 | "西葫芦", 123 | "西芹", 124 | "蟹味菇", 125 | "猪肚菇", 126 | "快菜", 127 | "豌豆", 128 | "鸡腿菇", 129 | "草菇", 130 | "竹荪", 131 | "荷兰豆", 132 | "银耳" 133 | ], 134 | "fruits": ["菠萝", "草莓", "火龙果", "荔枝", "枇杷", "桑葚", "山竹", "樱桃", "莲雾", "嘉宝果", "乌梅", "青梅"] 135 | }, 136 | "6": { 137 | "vegetables": [ 138 | "白萝卜", 139 | "菠菜", 140 | "彩椒", 141 | "茶树菇", 142 | "豆角", 143 | "大葱", 144 | "大蒜", 145 | "黄瓜", 146 | "茄子", 147 | "土豆", 148 | "西红柿", 149 | "圆白菜", 150 | "洋葱", 151 | "油菜", 152 | "茴香", 153 | "海带", 154 | "黄豆芽", 155 | "尖椒", 156 | "茭白", 157 | "芥蓝", 158 | "苦瓜", 159 | "空心菜", 160 | "绿豆芽", 161 | "青萝卜", 162 | "秋葵", 163 | "丝瓜", 164 | "生菜", 165 | "四季豆", 166 | "娃娃菜", 167 | "豌豆苗", 168 | "苋菜", 169 | "香菜", 170 | "西葫芦", 171 | "西芹", 172 | "紫甘蓝", 173 | "毛豆", 174 | "木耳菜", 175 | "猪肚菇", 176 | "快菜", 177 | "豌豆", 178 | "红薯叶", 179 | "鸡腿菇", 180 | "草菇", 181 | "竹荪", 182 | "黄花菜", 183 | "牛肝菌", 184 | "银耳" 185 | ], 186 | "fruits": [ 187 | "菠萝", 188 | "草莓", 189 | "黑莓", 190 | "火龙果", 191 | "蓝莓", 192 | "荔枝", 193 | "榴莲", 194 | "桑葚", 195 | "桃", 196 | "山竹", 197 | "杏", 198 | "西瓜", 199 | "香瓜", 200 | "樱桃", 201 | "杨梅", 202 | "椰子", 203 | "神秘果", 204 | "释迦果", 205 | "菠萝蜜", 206 | "莲雾", 207 | "红毛丹", 208 | "树莓", 209 | "蛇皮果", 210 | "火参果", 211 | "黑布林", 212 | "黄皮果", 213 | "伊丽莎白瓜", 214 | "乌梅" 215 | ] 216 | }, 217 | "7": { 218 | "vegetables": [ 219 | "白菜", 220 | "菠菜", 221 | "彩椒", 222 | "茶树菇", 223 | "豆角", 224 | "大葱", 225 | "大蒜", 226 | "黄瓜", 227 | "南瓜", 228 | "茄子", 229 | "土豆", 230 | "西红柿", 231 | "圆白菜", 232 | "洋葱", 233 | "猴头菇", 234 | "海带", 235 | "黄豆芽", 236 | "尖椒", 237 | "茭白", 238 | "豇豆", 239 | "芥蓝", 240 | "苦瓜", 241 | "空心菜", 242 | "苦菊", 243 | "菱角", 244 | "绿豆芽", 245 | "青萝卜", 246 | "秋葵", 247 | "丝瓜", 248 | "山药", 249 | "四季豆", 250 | "娃娃菜", 251 | "苋菜", 252 | "香菜", 253 | "西芹", 254 | "紫甘蓝", 255 | "玉米", 256 | "毛豆", 257 | "木耳菜", 258 | "猪肚菇", 259 | "快菜", 260 | "红薯叶", 261 | "草菇", 262 | "黄花菜", 263 | "牛肝菌", 264 | "银耳", 265 | "榛蘑" 266 | ], 267 | "fruits": [ 268 | "苹果", 269 | "菠萝", 270 | "黑莓", 271 | "火龙果", 272 | "哈密瓜", 273 | "龙眼", 274 | "蓝莓", 275 | "李子", 276 | "荔枝", 277 | "榴莲", 278 | "芒果", 279 | "蟠桃", 280 | "桃", 281 | "山竹", 282 | "杏", 283 | "西瓜", 284 | "香瓜", 285 | "油桃", 286 | "杨梅", 287 | "杨桃", 288 | "椰子", 289 | "释迦果", 290 | "菠萝蜜", 291 | "莲雾", 292 | "红毛丹", 293 | "树莓", 294 | "火参果", 295 | "嘉宝果", 296 | "黑布林", 297 | "黄皮果", 298 | "伊丽莎白瓜", 299 | "西洋梨" 300 | ] 301 | }, 302 | "8": { 303 | "vegetables": [ 304 | "白菜", 305 | "菠菜", 306 | "豆角", 307 | "冬瓜", 308 | "大葱", 309 | "大蒜", 310 | "黄瓜", 311 | "红薯", 312 | "南瓜", 313 | "茄子", 314 | "圆白菜", 315 | "猴头菇", 316 | "海带", 317 | "黄豆芽", 318 | "尖椒", 319 | "茭白", 320 | "豇豆", 321 | "荠菜", 322 | "芥蓝", 323 | "苦瓜", 324 | "苦菊", 325 | "口蘑", 326 | "菱角", 327 | "绿豆芽", 328 | "青萝卜", 329 | "芹菜", 330 | "秋葵", 331 | "丝瓜", 332 | "山药", 333 | "四季豆", 334 | "苋菜", 335 | "香菜", 336 | "西芹", 337 | "芋头", 338 | "油麦菜", 339 | "玉米", 340 | "紫薯", 341 | "毛豆", 342 | "猪肚菇", 343 | "牛蒡", 344 | "快菜", 345 | "红薯叶", 346 | "草菇", 347 | "黄花菜", 348 | "牛肝菌", 349 | "银耳", 350 | "榛蘑" 351 | ], 352 | "fruits": [ 353 | "苹果", 354 | "百香果", 355 | "黄桃", 356 | "火龙果", 357 | "哈密瓜", 358 | "龙眼", 359 | "蓝莓", 360 | "李子", 361 | "荔枝", 362 | "榴莲", 363 | "猕猴桃", 364 | "木瓜", 365 | "芒果", 366 | "葡萄", 367 | "蟠桃", 368 | "青提", 369 | "桃", 370 | "沙果", 371 | "山竹", 372 | "无花果", 373 | "西瓜", 374 | "油桃", 375 | "杨梅", 376 | "杨桃", 377 | "椰子", 378 | "释迦果", 379 | "菠萝蜜", 380 | "牛油果", 381 | "莲雾", 382 | "红毛丹", 383 | "火参果", 384 | "黑布林", 385 | "西梅", 386 | "黄皮果", 387 | "沙棘", 388 | "羊角蜜", 389 | "芭蕉", 390 | "西洋梨" 391 | ] 392 | }, 393 | "9": { 394 | "vegetables": [ 395 | "菠菜", 396 | "菜花", 397 | "豆角", 398 | "冬瓜", 399 | "大辣椒", 400 | "大蒜", 401 | "红薯", 402 | "姜", 403 | "南瓜", 404 | "圆白菜", 405 | "油菜", 406 | "猴头菇", 407 | "茴香", 408 | "海带", 409 | "黄豆芽", 410 | "尖椒", 411 | "茭白", 412 | "荠菜", 413 | "芥蓝", 414 | "芥菜", 415 | "苦瓜", 416 | "口蘑", 417 | "莲藕", 418 | "菱角", 419 | "绿豆芽", 420 | "青萝卜", 421 | "芹菜", 422 | "秋葵", 423 | "秋黄瓜", 424 | "丝瓜", 425 | "生菜", 426 | "山药", 427 | "四季豆", 428 | "香菜", 429 | "西芹", 430 | "芋头", 431 | "油麦菜", 432 | "玉米", 433 | "紫薯", 434 | "毛豆", 435 | "猪肚菇", 436 | "牛蒡", 437 | "快菜", 438 | "红薯叶", 439 | "鸡腿菇", 440 | "草菇", 441 | "黄花菜", 442 | "牛肝菌", 443 | "银耳", 444 | "腐竹" 445 | ], 446 | "fruits": [ 447 | "苹果", 448 | "百香果", 449 | "菇娘", 450 | "红提", 451 | "黄桃", 452 | "海棠果", 453 | "火龙果", 454 | "哈密瓜", 455 | "龙眼", 456 | "蓝莓", 457 | "李子", 458 | "蜜柑", 459 | "猕猴桃", 460 | "木瓜", 461 | "芒果", 462 | "葡萄", 463 | "蟠桃", 464 | "青提", 465 | "蛇果", 466 | "沙果", 467 | "柿子", 468 | "山竹", 469 | "石榴", 470 | "无花果", 471 | "西柚", 472 | "香梨", 473 | "梨", 474 | "西瓜", 475 | "杨桃", 476 | "枣", 477 | "人参果", 478 | "释迦果", 479 | "菠萝蜜", 480 | "牛油果", 481 | "人心果", 482 | "蔓越莓", 483 | "火参果", 484 | "南果梨", 485 | "冬枣", 486 | "沙棘", 487 | "羊角蜜", 488 | "芭蕉", 489 | "枣仁", 490 | "西洋梨" 491 | ] 492 | }, 493 | "10": { 494 | "vegetables": [ 495 | "白菜", 496 | "菠菜", 497 | "百合", 498 | "菜花", 499 | "豆角", 500 | "冬瓜", 501 | "大辣椒", 502 | "大蒜", 503 | "胡萝卜", 504 | "红薯", 505 | "姜", 506 | "木耳", 507 | "南瓜", 508 | "土豆", 509 | "西兰花", 510 | "圆白菜", 511 | "油菜", 512 | "茴香", 513 | "黄豆芽", 514 | "尖椒", 515 | "茭白", 516 | "荠菜", 517 | "芥蓝", 518 | "芥菜", 519 | "苦瓜", 520 | "莲藕", 521 | "绿豆芽", 522 | "魔芋", 523 | "青萝卜", 524 | "芹菜", 525 | "秋葵", 526 | "秋黄瓜", 527 | "丝瓜", 528 | "生菜", 529 | "山药", 530 | "四季豆", 531 | "茼蒿", 532 | "娃娃菜", 533 | "香菜", 534 | "雪莲果", 535 | "芋头", 536 | "油麦菜", 537 | "蟹味菇", 538 | "紫菜", 539 | "紫甘蓝", 540 | "玉米", 541 | "紫薯", 542 | "猪肚菇", 543 | "快菜", 544 | "豌豆", 545 | "雪里红", 546 | "鸡腿菇", 547 | "草菇", 548 | "牛肝菌", 549 | "腐竹" 550 | ], 551 | "fruits": [ 552 | "苹果", 553 | "菠萝", 554 | "百香果", 555 | "橄榄", 556 | "菇娘", 557 | "红提", 558 | "海棠果", 559 | "火龙果", 560 | "金橘", 561 | "橘子", 562 | "蜜柑", 563 | "猕猴桃", 564 | "木瓜", 565 | "葡萄", 566 | "脐橙", 567 | "蛇果", 568 | "柿子", 569 | "山楂", 570 | "石榴", 571 | "无花果", 572 | "西柚", 573 | "香梨", 574 | "梨", 575 | "香蕉", 576 | "柚子", 577 | "枣", 578 | "神秘果", 579 | "人参果", 580 | "释迦果", 581 | "菠萝蜜", 582 | "人心果", 583 | "番石榴", 584 | "蔓越莓", 585 | "火参果", 586 | "嘉宝果", 587 | "南果梨", 588 | "冬枣", 589 | "橙子", 590 | "沙棘", 591 | "羊角蜜", 592 | "芭蕉", 593 | "枣仁", 594 | "青枣" 595 | ] 596 | }, 597 | "11": { 598 | "vegetables": [ 599 | "白菜", 600 | "菠菜", 601 | "百合", 602 | "胡萝卜", 603 | "红薯", 604 | "姜", 605 | "木耳", 606 | "土豆", 607 | "西兰花", 608 | "圆白菜", 609 | "尖椒", 610 | "金针菇", 611 | "荠菜", 612 | "芥蓝", 613 | "芥菜", 614 | "秋黄瓜", 615 | "山药", 616 | "蒜黄", 617 | "茼蒿", 618 | "娃娃菜", 619 | "香菇", 620 | "雪莲果", 621 | "杏鲍菇", 622 | "蟹味菇", 623 | "紫菜", 624 | "紫甘蓝", 625 | "豆瓣菜", 626 | "快菜", 627 | "豌豆", 628 | "雪里红", 629 | "鸡腿菇", 630 | "腐竹" 631 | ], 632 | "fruits": [ 633 | "苹果", 634 | "菠萝", 635 | "橄榄", 636 | "甘蔗", 637 | "菇娘", 638 | "海棠果", 639 | "火龙果", 640 | "金橘", 641 | "罗汉果", 642 | "橘子", 643 | "脐橙", 644 | "砂糖橘", 645 | "蛇果", 646 | "山楂", 647 | "香蕉", 648 | "柚子", 649 | "枣", 650 | "人参果", 651 | "释迦果", 652 | "菠萝蜜", 653 | "番石榴", 654 | "蔓越莓", 655 | "冬枣", 656 | "橙子", 657 | "芭蕉", 658 | "青枣" 659 | ] 660 | }, 661 | "12": { 662 | "vegetables": [ 663 | "菠菜", 664 | "胡萝卜", 665 | "红薯", 666 | "土豆", 667 | "金针菇", 668 | "芥菜", 669 | "平菇", 670 | "蒜黄", 671 | "茼蒿", 672 | "香菇", 673 | "杏鲍菇", 674 | "紫菜", 675 | "豆瓣菜", 676 | "紫菜苔", 677 | "鸡腿菇" 678 | ], 679 | "fruits": ["菠萝", "橄榄", "甘蔗", "罗汉果", "橘子", "砂糖橘", "香蕉", "蛋黄果", "番石榴", "嘉宝果", "橙子", "青枣"] 680 | } 681 | } 682 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-object-type 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | declare module '*.json' { 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | const value: any 13 | export default value 14 | } 15 | -------------------------------------------------------------------------------- /src/helpers/counter.ts: -------------------------------------------------------------------------------- 1 | function* inc() { 2 | let index = 0 3 | 4 | while (true) { 5 | if (index === Number.MAX_SAFE_INTEGER) { 6 | index = 0 7 | } 8 | yield index++ 9 | } 10 | } 11 | 12 | export const counter = inc() 13 | -------------------------------------------------------------------------------- /src/helpers/date.ts: -------------------------------------------------------------------------------- 1 | export const getTime = (date: Date) => 2 | new Intl.DateTimeFormat('zh-CN', { 3 | hour: 'numeric', 4 | minute: 'numeric', 5 | second: 'numeric', 6 | hour12: false, 7 | }).format(date) 8 | 9 | export const getYear = (date: Date) => date.getFullYear() 10 | 11 | export const getMonth = (date: Date) => date.getMonth() + 1 12 | 13 | export const getDay = (date: Date) => date.getDate() 14 | 15 | export const getWeekday = (date: Date) => 16 | new Intl.DateTimeFormat('zh-CN', { 17 | weekday: 'long', 18 | }).format(date) 19 | 20 | export const getDate = (date: Date) => `${getYear(date)}-${getMonth(date)}-${getDay(date)}` 21 | -------------------------------------------------------------------------------- /src/helpers/random.ts: -------------------------------------------------------------------------------- 1 | export const shuffle = (array: string[]) => array.sort(() => Math.random() - 0.5) 2 | 3 | export const randomNumber = (min: number, max: number) => Math.round((Math.random() * (max - min) + min) * 100) / 100 4 | -------------------------------------------------------------------------------- /src/helpers/storage.ts: -------------------------------------------------------------------------------- 1 | const webGetItem = async (key: string): Promise => localStorage.getItem(key) 2 | 3 | const webSetItem = (key: string, value: string) => localStorage.setItem(key, value) 4 | 5 | const extGetItem = async (key: string): Promise => { 6 | const values = await chrome.storage.sync.get(key) 7 | 8 | return values[key] 9 | } 10 | 11 | const extSetItem = (key: string, value: string) => chrome.storage.sync.set({ [key]: value }) 12 | 13 | const isWebExt = window?.chrome?.runtime?.id !== undefined 14 | 15 | export const storage = { 16 | getItem: isWebExt ? extGetItem : webGetItem, 17 | setItem: isWebExt ? extSetItem : webSetItem, 18 | } 19 | -------------------------------------------------------------------------------- /src/helpers/style.ts: -------------------------------------------------------------------------------- 1 | export const measureParagraph = (container: HTMLElement, content: string) => { 2 | const dataAttr = container.getAttributeNames().find((i) => i.startsWith('data-')) 3 | const tempContainer = document.createElement('div') 4 | const htmlString = `

    ${content}

    ` 5 | 6 | tempContainer.innerHTML = htmlString 7 | 8 | const p = tempContainer.firstElementChild 9 | 10 | if (!p) return { width: 0, height: 0 } 11 | 12 | container.appendChild(p) 13 | 14 | const computedStyle = window.getComputedStyle(p) 15 | const width = Math.round( 16 | parseFloat(computedStyle.width) + 17 | parseFloat(computedStyle.paddingLeft) + 18 | parseFloat(computedStyle.paddingRight) + 19 | parseFloat(computedStyle.borderLeftWidth) + 20 | parseFloat(computedStyle.borderRightWidth) + 21 | parseFloat(computedStyle.marginLeft) + 22 | parseFloat(computedStyle.marginRight) 23 | ) 24 | const height = Math.round( 25 | parseFloat(computedStyle.height) + 26 | parseFloat(computedStyle.paddingTop) + 27 | parseFloat(computedStyle.paddingBottom) + 28 | parseFloat(computedStyle.borderTopWidth) + 29 | parseFloat(computedStyle.borderBottomWidth) + 30 | parseFloat(computedStyle.marginTop) + 31 | parseFloat(computedStyle.marginBottom) 32 | ) 33 | 34 | container.removeChild(p) 35 | 36 | return { width, height } 37 | } 38 | 39 | export const isParagraphMultipleLinesInW3 = (container: HTMLElement, content: string) => { 40 | const dataAttr = container.getAttributeNames().find((i) => i.startsWith('data-')) 41 | const tempContainer = document.createElement('div') 42 | const htmlString = `

    ${content}

    ` 43 | 44 | tempContainer.innerHTML = htmlString 45 | 46 | const p = tempContainer.firstElementChild 47 | 48 | if (!p) return false 49 | 50 | container.appendChild(p) 51 | 52 | const computedStyle = window.getComputedStyle(p) 53 | const lineHeight = parseInt(computedStyle.lineHeight, 10) 54 | const width = parseInt(computedStyle.width, 10) 55 | 56 | container.removeChild(p) 57 | 58 | return lineHeight < width 59 | } 60 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /src/services/ApiService.ts: -------------------------------------------------------------------------------- 1 | export class ApiService { 2 | resource: string 3 | 4 | constructor(resource: string) { 5 | if (!resource) { 6 | throw new Error('Resource is not provided') 7 | } 8 | this.resource = resource 9 | } 10 | 11 | getUrl() { 12 | return this.resource 13 | } 14 | 15 | handleError(err: unknown) { 16 | console.error({ error: err }) 17 | } 18 | 19 | async fetch(config = {}) { 20 | try { 21 | const response = await fetch(this.getUrl(), config) 22 | 23 | return await response.json() 24 | } catch (error) { 25 | this.handleError(error) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/services/LatestUpdateService.ts: -------------------------------------------------------------------------------- 1 | import { ApiService } from './ApiService' 2 | 3 | export class LatestUpdateService extends ApiService { 4 | constructor() { 5 | super(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/latest-update`) 6 | } 7 | 8 | async fetch(config: RequestInit = {}) { 9 | return super.fetch({ 10 | ...config, 11 | headers: { 12 | ...config.headers, 13 | Authorization: `Bearer ${import.meta.env.VITE_SUPABASE_ANON_KEY}`, 14 | }, 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/services/NextHolidayService.ts: -------------------------------------------------------------------------------- 1 | import { ApiService } from './ApiService' 2 | 3 | export class NextHolidayService extends ApiService { 4 | constructor(date: string) { 5 | super(`https://timor.tech/api/holiday/next/${date}?week=Y`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/services/TodayInfoService.ts: -------------------------------------------------------------------------------- 1 | import { ApiService } from './ApiService' 2 | 3 | export class TodayInfoService extends ApiService { 4 | constructor(date: string) { 5 | super(`https://timor.tech/api/holiday/info/${date}`) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Month = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | '11' | '12' 2 | 3 | export type Day = 4 | | '1' 5 | | '2' 6 | | '3' 7 | | '4' 8 | | '5' 9 | | '6' 10 | | '7' 11 | | '8' 12 | | '9' 13 | | '10' 14 | | '11' 15 | | '12' 16 | | '13' 17 | | '14' 18 | | '15' 19 | | '16' 20 | | '17' 21 | | '18' 22 | | '19' 23 | | '20' 24 | | '21' 25 | | '22' 26 | | '23' 27 | | '24' 28 | | '25' 29 | | '26' 30 | | '27' 31 | | '28' 32 | | '29' 33 | | '30' 34 | | '31' 35 | 36 | type MonthInHistory = Partial> 37 | 38 | export type YearInHistory = Record 39 | 40 | export type SeasonFood = Record< 41 | Month, 42 | { 43 | vegetables: string[] 44 | fruits: string[] 45 | } 46 | > 47 | 48 | export interface IContentBaseProps { 49 | date: Date 50 | } 51 | 52 | export enum Mode { 53 | Full = 'full', 54 | Continuous = 'continuous', 55 | Soft = 'soft', 56 | } 57 | 58 | export interface IModeConfig { 59 | turn: () => number 60 | isReversed: boolean 61 | interval: number 62 | } 63 | 64 | export interface IModeConfigValue { 65 | turn: number 66 | isReversed: boolean 67 | interval: number 68 | } 69 | 70 | export interface ILatestUpdate { 71 | message: string 72 | } 73 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | // Brand Colors 2 | $color-primary: #f7f4e3; // Main brand color, light beige 3 | $color-accent: #8f8148; // Secondary accent color, gold brown 4 | $color-danger: #e81c27; // Error/danger color, red 5 | 6 | // Text Colors 7 | $color-text-dark: #0c0c0a; // Dark text for contrast 8 | $color-text-light: #fefdf5; // Light text for dark backgrounds 9 | 10 | // UI Colors 11 | $color-shadow: rgba(143, 129, 72, 0.15); // Accent-based shadow for depth 12 | -------------------------------------------------------------------------------- /src/views/ContentView.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 35 | 36 | 95 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # For detailed configuration reference documentation, visit: 2 | # https://supabase.com/docs/guides/local-development/cli/config 3 | # A string used to distinguish different Supabase projects on the same host. Defaults to the 4 | # working directory name when running `supabase init`. 5 | project_id = "wai" 6 | 7 | [api] 8 | enabled = true 9 | # Port to use for the API URL. 10 | port = 54321 11 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 12 | # endpoints. `public` and `graphql_public` schemas are included by default. 13 | schemas = ["public", "graphql_public"] 14 | # Extra schemas to add to the search_path of every request. 15 | extra_search_path = ["public", "extensions"] 16 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 17 | # for accidental or malicious requests. 18 | max_rows = 1000 19 | 20 | [api.tls] 21 | # Enable HTTPS endpoints locally using a self-signed certificate. 22 | enabled = false 23 | 24 | [db] 25 | # Port to use for the local database URL. 26 | port = 54322 27 | # Port used by db diff command to initialize the shadow database. 28 | shadow_port = 54320 29 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 30 | # server_version;` on the remote database to check. 31 | major_version = 15 32 | 33 | [db.pooler] 34 | enabled = false 35 | # Port to use for the local connection pooler. 36 | port = 54329 37 | # Specifies when a server connection can be reused by other clients. 38 | # Configure one of the supported pooler modes: `transaction`, `session`. 39 | pool_mode = "transaction" 40 | # How many server connections to allow per user/database pair. 41 | default_pool_size = 20 42 | # Maximum number of client connections allowed. 43 | max_client_conn = 100 44 | 45 | # [db.vault] 46 | # secret_key = "env(SECRET_VALUE)" 47 | 48 | [db.migrations] 49 | # Specifies an ordered list of schema files that describe your database. 50 | # Supports glob patterns relative to supabase directory: "./schemas/*.sql" 51 | schema_paths = [] 52 | 53 | [db.seed] 54 | # If enabled, seeds the database after migrations during a db reset. 55 | enabled = true 56 | # Specifies an ordered list of seed files to load during db reset. 57 | # Supports glob patterns relative to supabase directory: "./seeds/*.sql" 58 | sql_paths = ["./seed.sql"] 59 | 60 | [realtime] 61 | enabled = true 62 | # Bind realtime via either IPv4 or IPv6. (default: IPv4) 63 | # ip_version = "IPv6" 64 | # The maximum length in bytes of HTTP request headers. (default: 4096) 65 | # max_header_length = 4096 66 | 67 | [studio] 68 | enabled = true 69 | # Port to use for Supabase Studio. 70 | port = 54323 71 | # External URL of the API server that frontend connects to. 72 | api_url = "http://127.0.0.1" 73 | # OpenAI API Key to use for Supabase AI in the Supabase Studio. 74 | openai_api_key = "env(OPENAI_API_KEY)" 75 | 76 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 77 | # are monitored, and you can view the emails that would have been sent from the web interface. 78 | [inbucket] 79 | enabled = true 80 | # Port to use for the email testing server web interface. 81 | port = 54324 82 | # Uncomment to expose additional ports for testing user applications that send emails. 83 | # smtp_port = 54325 84 | # pop3_port = 54326 85 | # admin_email = "admin@email.com" 86 | # sender_name = "Admin" 87 | 88 | [storage] 89 | enabled = true 90 | # The maximum file size allowed (e.g. "5MB", "500KB"). 91 | file_size_limit = "50MiB" 92 | 93 | # Image transformation API is available to Supabase Pro plan. 94 | # [storage.image_transformation] 95 | # enabled = true 96 | 97 | # Uncomment to configure local storage buckets 98 | # [storage.buckets.images] 99 | # public = false 100 | # file_size_limit = "50MiB" 101 | # allowed_mime_types = ["image/png", "image/jpeg"] 102 | # objects_path = "./images" 103 | 104 | [auth] 105 | enabled = true 106 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 107 | # in emails. 108 | site_url = "http://127.0.0.1:3000" 109 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 110 | additional_redirect_urls = ["https://127.0.0.1:3000"] 111 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). 112 | jwt_expiry = 3600 113 | # If disabled, the refresh token will never expire. 114 | enable_refresh_token_rotation = true 115 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. 116 | # Requires enable_refresh_token_rotation = true. 117 | refresh_token_reuse_interval = 10 118 | # Allow/disallow new user signups to your project. 119 | enable_signup = true 120 | # Allow/disallow anonymous sign-ins to your project. 121 | enable_anonymous_sign_ins = false 122 | # Allow/disallow testing manual linking of accounts 123 | enable_manual_linking = false 124 | # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. 125 | minimum_password_length = 6 126 | # Passwords that do not meet the following requirements will be rejected as weak. Supported values 127 | # are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` 128 | password_requirements = "" 129 | 130 | # Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. 131 | # [auth.captcha] 132 | # enabled = true 133 | # provider = "hcaptcha" 134 | # secret = "" 135 | 136 | [auth.email] 137 | # Allow/disallow new user signups via email to your project. 138 | enable_signup = true 139 | # If enabled, a user will be required to confirm any email change on both the old, and new email 140 | # addresses. If disabled, only the new email is required to confirm. 141 | double_confirm_changes = true 142 | # If enabled, users need to confirm their email address before signing in. 143 | enable_confirmations = false 144 | # If enabled, users will need to reauthenticate or have logged in recently to change their password. 145 | secure_password_change = false 146 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. 147 | max_frequency = "1s" 148 | # Number of characters used in the email OTP. 149 | otp_length = 6 150 | # Number of seconds before the email OTP expires (defaults to 1 hour). 151 | otp_expiry = 3600 152 | 153 | # Use a production-ready SMTP server 154 | # [auth.email.smtp] 155 | # enabled = true 156 | # host = "smtp.sendgrid.net" 157 | # port = 587 158 | # user = "apikey" 159 | # pass = "env(SENDGRID_API_KEY)" 160 | # admin_email = "admin@email.com" 161 | # sender_name = "Admin" 162 | 163 | # Uncomment to customize email template 164 | # [auth.email.template.invite] 165 | # subject = "You have been invited" 166 | # content_path = "./supabase/templates/invite.html" 167 | 168 | [auth.sms] 169 | # Allow/disallow new user signups via SMS to your project. 170 | enable_signup = false 171 | # If enabled, users need to confirm their phone number before signing in. 172 | enable_confirmations = false 173 | # Template for sending OTP to users 174 | template = "Your code is {{ .Code }}" 175 | # Controls the minimum amount of time that must pass before sending another sms otp. 176 | max_frequency = "5s" 177 | 178 | # Use pre-defined map of phone number to OTP for testing. 179 | # [auth.sms.test_otp] 180 | # 4152127777 = "123456" 181 | 182 | # Configure logged in session timeouts. 183 | # [auth.sessions] 184 | # Force log out after the specified duration. 185 | # timebox = "24h" 186 | # Force log out if the user has been inactive longer than the specified duration. 187 | # inactivity_timeout = "8h" 188 | 189 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. 190 | # [auth.hook.custom_access_token] 191 | # enabled = true 192 | # uri = "pg-functions:////" 193 | 194 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. 195 | [auth.sms.twilio] 196 | enabled = false 197 | account_sid = "" 198 | message_service_sid = "" 199 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: 200 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" 201 | 202 | # Multi-factor-authentication is available to Supabase Pro plan. 203 | [auth.mfa] 204 | # Control how many MFA factors can be enrolled at once per user. 205 | max_enrolled_factors = 10 206 | 207 | # Control MFA via App Authenticator (TOTP) 208 | [auth.mfa.totp] 209 | enroll_enabled = false 210 | verify_enabled = false 211 | 212 | # Configure MFA via Phone Messaging 213 | [auth.mfa.phone] 214 | enroll_enabled = false 215 | verify_enabled = false 216 | otp_length = 6 217 | template = "Your code is {{ .Code }}" 218 | max_frequency = "5s" 219 | 220 | # Configure MFA via WebAuthn 221 | # [auth.mfa.web_authn] 222 | # enroll_enabled = true 223 | # verify_enabled = true 224 | 225 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 226 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, 227 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 228 | [auth.external.apple] 229 | enabled = false 230 | client_id = "" 231 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: 232 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" 233 | # Overrides the default auth redirectUrl. 234 | redirect_uri = "" 235 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 236 | # or any other third-party OIDC providers. 237 | url = "" 238 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth. 239 | skip_nonce_check = false 240 | 241 | # Use Firebase Auth as a third-party provider alongside Supabase Auth. 242 | [auth.third_party.firebase] 243 | enabled = false 244 | # project_id = "my-firebase-project" 245 | 246 | # Use Auth0 as a third-party provider alongside Supabase Auth. 247 | [auth.third_party.auth0] 248 | enabled = false 249 | # tenant = "my-auth0-tenant" 250 | # tenant_region = "us" 251 | 252 | # Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. 253 | [auth.third_party.aws_cognito] 254 | enabled = false 255 | # user_pool_id = "my-user-pool-id" 256 | # user_pool_region = "us-east-1" 257 | 258 | [edge_runtime] 259 | enabled = true 260 | # Configure one of the supported request policies: `oneshot`, `per_worker`. 261 | # Use `oneshot` for hot reload, or `per_worker` for load testing. 262 | policy = "oneshot" 263 | # Port to attach the Chrome inspector for debugging edge functions. 264 | inspector_port = 8083 265 | 266 | # Use these configurations to customize your Edge Function. 267 | # [functions.MY_FUNCTION_NAME] 268 | # enabled = true 269 | # verify_jwt = true 270 | # import_map = "./functions/MY_FUNCTION_NAME/deno.json" 271 | # Uncomment to specify a custom file path to the entrypoint. 272 | # Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx 273 | # entrypoint = "./functions/MY_FUNCTION_NAME/index.ts" 274 | # Specifies static files to be bundled with the function. Supports glob patterns. 275 | # For example, if you want to serve static HTML pages in your function: 276 | # static_files = [ "./functions/MY_FUNCTION_NAME/*.html" ] 277 | 278 | [analytics] 279 | enabled = true 280 | port = 54327 281 | # Configure one of the supported backends: `postgres`, `bigquery`. 282 | backend = "postgres" 283 | 284 | # Experimental features may be deprecated any time 285 | [experimental] 286 | # Configures Postgres storage engine to use OrioleDB (S3) 287 | orioledb_version = "" 288 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com 289 | s3_host = "env(S3_HOST)" 290 | # Configures S3 bucket region, eg. us-east-1 291 | s3_region = "env(S3_REGION)" 292 | # Configures AWS_ACCESS_KEY_ID for S3 bucket 293 | s3_access_key = "env(S3_ACCESS_KEY)" 294 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket 295 | s3_secret_key = "env(S3_SECRET_KEY)" 296 | -------------------------------------------------------------------------------- /supabase/functions/latest-update/.npmrc: -------------------------------------------------------------------------------- 1 | # Configuration for private npm package dependencies 2 | # For more information on using private registries with Edge Functions, see: 3 | # https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries 4 | -------------------------------------------------------------------------------- /supabase/functions/latest-update/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": {} 3 | } 4 | -------------------------------------------------------------------------------- /supabase/functions/latest-update/index.ts: -------------------------------------------------------------------------------- 1 | // Follow this setup guide to integrate the Deno language server with your editor: 2 | // https://deno.land/manual/getting_started/setup_your_environment 3 | // This enables autocomplete, go to definition, etc. 4 | 5 | // Setup type definitions for built-in Supabase Runtime APIs 6 | import 'jsr:@supabase/functions-js/edge-runtime.d.ts' 7 | 8 | Deno.serve(() => { 9 | const data = { 10 | message: '', 11 | } 12 | 13 | return new Response(JSON.stringify(data), { 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | 'Access-Control-Allow-Origin': '*', 17 | 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 18 | 'Cache-Control': 'public, max-age=86400', 19 | Expires: new Date(Date.now() + 86400000).toUTCString(), // 24 hours from now 20 | 'Last-Modified': new Date().toUTCString(), 21 | }, 22 | }) 23 | }) 24 | 25 | /* To invoke locally: 26 | 27 | 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) 28 | 2. Make an HTTP request: 29 | 30 | curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/latest-update' \ 31 | --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ 32 | --header 'Content-Type: application/json' \ 33 | --data '{"name":"Functions"}' 34 | 35 | */ 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": [ 14 | "esnext", 15 | "dom" 16 | ], 17 | "skipLibCheck": true 18 | }, 19 | "include": [ 20 | "src/**/*.ts", 21 | "src/**/*.d.ts", 22 | "src/**/*.vue" 23 | ], 24 | "references": [ 25 | { 26 | "path": "./tsconfig.node.json" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": [ 8 | "vite.config.ts", 9 | "manifest.config.ts", 10 | "scripts/**/*.ts", 11 | ] 12 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | import banner from 'vite-plugin-banner' 4 | import { viteBuildManifest } from './scripts/build-manifest' 5 | 6 | export default defineConfig({ 7 | plugins: [vue(), banner('@author Huan Luo (https://shaiwang.life)'), viteBuildManifest()], 8 | build: { 9 | chunkSizeWarningLimit: 4096, 10 | }, 11 | }) 12 | --------------------------------------------------------------------------------