diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 100% rename from .eslintrc.js rename to .eslintrc.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 612b4bf6aad..c67c4d26ef3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,13 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: ['3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} id: setup_python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 32cb21ef5c1..af7a74cddbc 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -46,14 +46,14 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # if your default branches is not master, please change it here ref: master - name: Cache Data Files if: inputs.save_data_in_github_cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | activities @@ -71,9 +71,9 @@ jobs: ${{ inputs.data_cache_prefix }}- - name: Setup Node.js environment - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '18' + node-version: '20' - uses: pnpm/action-setup@v2 name: Install pnpm @@ -86,7 +86,7 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - uses: actions/cache@v3 + - uses: actions/cache@v4 name: Setup pnpm cache with: path: ${{ env.STORE_PATH }} @@ -101,10 +101,10 @@ jobs: run: PATH_PREFIX=/ pnpm build - name: Upload artifact - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v2 with: # Upload dist repository path: './dist' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v1 + uses: actions/deploy-pages@v3 diff --git a/.github/workflows/run_data_sync.yml b/.github/workflows/run_data_sync.yml index 5fa729965b6..588d7b2def7 100644 --- a/.github/workflows/run_data_sync.yml +++ b/.github/workflows/run_data_sync.yml @@ -13,15 +13,19 @@ on: - run_page/strava_sync.py - run_page/gen_svg.py - run_page/garmin_sync.py + - run_page/coros_sync.py - run_page/keep_sync.py - run_page/gpx_sync.py - run_page/tcx_sync.py + - run_page/tcx_to_garmin_sync.py - run_page/garmin_to_strava_sync.py + - run_page/keep_to_strava_sync.py + - run_page/oppo_sync.py - requirements.txt env: # please change to your own config. - RUN_TYPE: strava # support strava/nike/garmin/garmin_cn/keep/only_gpx/only_fit/nike_to_strava/strava_to_garmin/strava_to_garmin_cn/garmin_to_strava/garmin_to_strava_cn/codoon, Please change the 'pass' it to your own + RUN_TYPE: strava # support strava/nike/garmin/coros/garmin_cn/garmin_sync_cn_global/keep/only_gpx/only_fit/nike_to_strava/strava_to_garmin/tcx_to_garmin/strava_to_garmin_cn/garmin_to_strava/garmin_to_strava_cn/codoon/oppo, Please change the 'pass' it to your own ATHLETE: chensoul TITLE: Chensoul Running MIN_GRID_DISTANCE: 10 # change min distance here @@ -49,11 +53,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python id: setup_python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.10' cache: pip @@ -65,7 +69,7 @@ jobs: - name: Cache Data Files if: env.SAVE_DATA_IN_GITHUB_CACHE == 'true' - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | activities @@ -97,6 +101,17 @@ jobs: run: | python run_page/keep_sync.py ${{ secrets.KEEP_MOBILE }} ${{ secrets.KEEP_PASSWORD }} --with-gpx + - name: Run sync Coros script + if: env.RUN_TYPE == 'coros' + run: | + python run_page/coros_sync.py ${{ secrets.COROS_ACCOUNT }} ${{ secrets.COROS_PASSWORD }} + + - name: Run sync Keep_to_strava script + if: env.RUN_TYPE == 'keep_to_strava_sync' + run: | + python run_page/keep_to_strava_sync.py ${{ secrets.KEEP_MOBILE }} ${{ secrets.KEEP_PASSWORD }} ${{ secrets.STRAVA_CLIENT_ID }} ${{ secrets.STRAVA_CLIENT_SECRET }} ${{ secrets.STRAVA_CLIENT_REFRESH_TOKEN }} --sync-types running cycling hiking + # If you only want to sync `type running` modify args --sync-types running, default script is to sync all data (rides, hikes and runs). + - name: Run sync Strava script if: env.RUN_TYPE == 'strava' run: | @@ -108,6 +123,12 @@ jobs: run: | python run_page/codoon_sync.py ${{ secrets.CODOON_MOBILE }} ${{ secrets.CODOON_PASSWORD }} + - name: Run sync tcx to Garmin script + if: env.RUN_TYPE == 'tcx_to_garmin' + run: | + # python run_page/tcx_to_garmin_sync.py ${{ secrets.GARMIN_SECRET_STRING }} + python run_page/tcx_to_garmin_sync.py ${{ secrets.GARMIN_SECRET_STRING_CN }} --is-cn + # for garmin if you want generate `tcx` you can add --tcx command in the args. - name: Run sync Garmin script if: env.RUN_TYPE == 'garmin' @@ -133,7 +154,7 @@ jobs: # If you only want to sync `type running` add args --only-run, default script is to sync all data (rides and runs). # python run_page/garmin_sync_cn_global.py ${{ secrets.GARMIN_SECRET_STRING_CN }} ${{ secrets.GARMIN_SECRET_STRING }} --only-run - + - name: Run sync Only GPX script if: env.RUN_TYPE == 'only_gpx' run: | @@ -174,6 +195,13 @@ jobs: run: | python run_page/tulipsport_sync.py ${{ secrets.TULIPSPORT_TOKEN }} --with-gpx + - name: Run sync Oppo heytap script, note currently this script is not worked + if: env.RUN_TYPE == 'oppo' + run: | + python run_page/oppo_sync.py ${{ secrets.OPPO_ID }} ${{ secrets.OPPO_CLIENT_SECRET }} ${{ secrets.OPPO_CLIENT_REFRESH_TOKEN }} --with-tcx + # If you want to sync fit activity in gpx format, please consider the following script: + # python run_page/oppo_sync.py ${{ secrets.OPPO_ID }} ${{ secrets.OPPO_CLIENT_SECRET }} ${{ secrets.OPPO_CLIENT_REFRESH_TOKEN }} --with-gpx + - name: Make svg GitHub profile if: env.RUN_TYPE != 'pass' run: | diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 70% rename from .prettierrc.js rename to .prettierrc.cjs index c4e31cdad73..e2507d6d9f3 100644 --- a/.prettierrc.js +++ b/.prettierrc.cjs @@ -3,4 +3,5 @@ module.exports = { semi: true, bracketSpacing: true, singleQuote: true, + plugins: ['prettier-plugin-tailwindcss'] }; diff --git a/.vercelignore b/.vercelignore index 979f16f7011..8e294ae2f2e 100644 --- a/.vercelignore +++ b/.vercelignore @@ -1 +1,2 @@ requirements.txt +requirements-dev.txt diff --git a/Dockerfile b/Dockerfile index 684004d4391..9e9ff275037 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,9 @@ FROM python:3.10.5-slim AS develop-py WORKDIR /root/running_page COPY ./requirements.txt /root/running_page/requirements.txt +# Add proxy for apt. +# ENV http_proxy http://ip_address:port +# ENV https_proxy http://ip_address:port RUN apt-get update \ && apt-get install -y --no-install-recommends git \ && apt-get purge -y --auto-remove \ @@ -14,12 +17,10 @@ FROM node:18 AS develop-node WORKDIR /root/running_page COPY ./package.json /root/running_page/package.json COPY ./pnpm-lock.yaml /root/running_page/pnpm-lock.yaml -RUN npm config set registry https://registry.npm.taobao.org \ +RUN npm config rm proxy&&npm config set registry https://registry.npmjs.org/ \ &&npm install -g corepack \ &&corepack enable \ - &&pnpm install - - + &&yarn install FROM develop-py AS data ARG app @@ -29,6 +30,8 @@ ARG client_id ARG client_secret ARG refresh_token ARG YOUR_NAME +ARG keep_phone_number +ARG keep_password WORKDIR /root/running_page COPY . /root/running_page/ @@ -45,6 +48,8 @@ RUN DUMMY=${DUMMY}; \ python3 run_page/strava_sync.py ${client_id} ${client_secret} ${refresh_token};\ elif [ "$app" = "Nike_to_Strava" ] ; then \ python3 run_page/nike_to_strava_sync.py ${nike_refresh_token} ${client_id} ${client_secret} ${refresh_token};\ + elif [ "$app" = "Keep" ] ; then \ + python3 run_page/keep_sync.py ${keep_phone_number} ${keep_password};\ else \ echo "Unknown app" ; \ fi @@ -56,7 +61,7 @@ RUN python3 run_page/gen_svg.py --from-db --title "my running page" --type grid FROM develop-node AS frontend-build WORKDIR /root/running_page COPY --from=data /root/running_page /root/running_page -RUN pnpm run build +RUN yarn run build FROM nginx:alpine AS web COPY --from=frontend-build /root/running_page/dist /usr/share/nginx/html/ diff --git a/README-CN.md b/README-CN.md index c789e7d9eaf..933db5a4b37 100644 --- a/README-CN.md +++ b/README-CN.md @@ -42,7 +42,7 @@ R.I.P. 希望大家都能健康顺利的跑过终点,逝者安息。 | [zhubao315](https://github.com/zhubao315) | | Strava | | [shaonianche](https://github.com/shaonianche) | | Strava | | [yihong0618](https://github.com/yihong0618) | | Nike | -| [superleeyom](https://github.com/superleeyom) | | Nike | +| [superleeyom](https://github.com/superleeyom) | | Strava | | [geekplux](https://github.com/geekplux) | | Nike | | [guanlan](https://github.com/guanlan) | | Strava | | [tuzimoe](https://github.com/tuzimoe) | | Nike | @@ -96,7 +96,13 @@ R.I.P. 希望大家都能健康顺利的跑过终点,逝者安息。 | [Jeffggmm](https://github.com/Jeffggmm) | | Garmin | | [s1smart](https://github.com/s1smart) | | Strava | | [Ryan](https://github.com/85Ryan) | | Strava | -| [PPZ](https://github.com/8824PPZ) | | Strava | +| [PPZ](https://github.com/8824PPZ) | | Strava | +| [Yer1k](https://github.com/Yer1k) | | Strava | +| [AlienVision](https://github.com/weaming) | | Strava | +| [Vensent](https://github.com/Vensent) | | Garmin | +| [Zeonsing](https://github.com/NoonieBao) | | Coros | +| [yaoper](https://github.com/yaoper) | | codoon | +| [NoZTurn](https://github.com/NoZTurn) | | Strava | ## 它是怎么工作的 @@ -139,11 +145,12 @@ R.I.P. 希望大家都能健康顺利的跑过终点,逝者安息。 - **[FIT](#fit)** - **[佳明国内同步国际](#Garmin-CN-to-Garmin)** - **[Tcx+Strava(upload all tcx data to strava)](#tcx_to_strava)** +- **[Tcx+Garmin(upload all tcx data to Garmin)](#tcx_to_garmin)** - **[Gpx+Strava(upload all tcx data to strava)](#gpx_to_strava)** - **[Nike+Strava(Using NRC Run, Strava backup data)](#nikestrava)** - **[Garmin_to_Strava(Using Garmin Run, Strava backup data)](#garmin_to_strava)** - **[Strava_to_Garmin(Using Strava Run, Garmin backup data)](#strava_to_garmin)** - +- **[Coros高驰](#Coros高驰)** ## 视频教程 - https://www.youtube.com/watch?v=reLiY9p8EJk @@ -183,6 +190,9 @@ docker build -t running_page:latest . --build-arg app=Strava --build-arg client_ #Nike_to_Strava docker build -t running_page:latest . --build-arg app=Nike_to_Strava --build-arg nike_refresh_token="" --build-arg client_id="" --build-arg client_secret="" --build-arg refresh_token="" +# Keep +docker build -t running_page:latest . --build-arg app=Keep --build-arg keep_phone_number="" --build-arg keep_password="" + #启动 docker run -itd -p 80:80 running_page:latest @@ -232,6 +242,11 @@ siteMetadata: { const USE_DASH_LINE = true; // styling: 透明度:[0, 1] const LINE_OPACITY = 0.4; +// styling: 开启隐私模式(不显示地图仅显示轨迹): 设置为 `true` +// 注意:此配置仅影响页面显示,数据保护请参考下方的 "隐私保护" +const PRIVACY_MODE = false; +// styling: 默认关灯: 设置为 `false`, 仅在隐私模式关闭时生效(`PRIVACY_MODE` = false) +const LIGHTS_ON = true; ``` > 隐私保护:设置下面环境变量: @@ -321,7 +336,7 @@ python3(python) run_page/keep_sync.py ${your mobile} ${your password} python3(python) run_page/keep_sync.py 13333xxxx example ``` -> 我增加了 keep 可以导出 gpx 功能(因 keep 的原因,距离和速度会有一定缺失), 执行如下命令,导出的 gpx 会加入到 GPX_OUT 中,方便上传到其它软件 +> 我增加了 keep 可以导出 gpx 功能(因 keep 的原因,距离和速度会有一定缺失), 执行如下命令,导出的 gpx 会加入到 GPX_OUT 中,方便上传到其它软件。 ```bash python3(python) run_page/keep_sync.py ${your mobile} ${your password} --with-gpx @@ -330,9 +345,22 @@ python3(python) run_page/keep_sync.py ${your mobile} ${your password} --with-gpx 示例: ```bash -python3(python) run_page/keep_sync.py 13333xxxx example --with-gpx +python3(python) run_page/keep_sync.py 13333xxxx example --with-gpx +``` + +> 增加了 keep 对其他运动类型的支持,目前可选的有running, cycling, hiking,默认的运动数据类型为running。 + +```bash +python3(python) run_page/keep_sync.py ${your mobile} ${your password} --with-gpx --sync-types running cycling hiking +``` + +示例: + +```bash +python3(python) run_page/keep_sync.py 13333xxxx example --with-gpx --sync-types running cycling hiking ``` +
@@ -752,6 +780,31 @@ python3(python) run_page/tcx_to_strava_sync.py xxx xxx xxx --all > 如果你已经上传过需要跳过判断增加参数 `--all` +### TCX_to_Garmin + +
+上传所有的 tcx 格式的跑步数据到 Garmin + +
+ +1. 完成 garmin 的步骤 +2. 把 tcx 文件全部拷贝到 TCX_OUT 中 +3. 在项目根目录下执行: + +```bash +python3 run_page/tcx_to_garmin_sync.py ${{ secrets.GARMIN_SECRET_STRING_CN }} --is-cn +``` + +示例: + +```bash +python run_page/tcx_to_garmin_sync.py xxx --is-cn +或佳明国际 +python run_page/tcx_to_garmin_sync.py xxx +``` + +> 如果你已经上传过需要跳过判断增加参数 `--all` +
### GPX_to_Strava @@ -859,6 +912,53 @@ python3(python) run_page/strava_to_garmin_sync.py ${{ secrets.STRAVA_CLIENT_ID }
+### Coros高驰 + +
+获取您的 Coros高驰 数据 + +#### 在终端中输入以下命令 + +```bash +python run_page/coros_sync.py ${{ secrets.COROS_ACCOUNT }} ${{ secrets.COROS_PASSWORD }} +``` + +#### 修改 `run_data_sync.yml` 中 `env.RUN_TYPE: coros` + +#### 设置 github action中Coros高驰信息 + +- 在github action中配置`COROS_ACCOUNT`,`COROS_PASSWORD`参数 + + ![github-action](https://img3.uploadhouse.com/fileuploads/30980/3098042335f8995623f8b50776c4fad4cf7fff8d.png) + +
+ +### Keep_to_Strava +
+获取您的Keep数据,然后同步到Strava + +示例: +```bash +python3(python) run_page/keep_to_strava_sync.py ${your mobile} ${your password} ${client_id} ${client_secret} ${strava_refresh_token} --sync-types running cycling hiking +``` + +#### 解决的需求: +1. 适用于由Strava总览/展示数据,但是有多种运动类型,且数据来自不同设备的用户。 +2. 适用于期望将华为运动健康/OPPO健康等数据同步到Strava的用户(前提是手机APP端已经开启了和Keep之间的数据同步)。 +3. 理论上华为/OPPO等可以通过APP同步到Keep的设备,均可通过此方法自动同步到Strava,目前已通过测试的APP有 + - 华为运动健康: 户外跑步,户外骑行,户外步行。 + +#### 特性以及使用细节: +1. 与Keep相似,但是由keep_to_strava_sync.py实现,不侵入data.db 与 activities.json。因此不会出现由于同时使用keep_sync和strava_sync而导致的数据重复统计/展示问题。 +2. 上传至Strava时,会自动识别为Strava中相应的运动类型, 目前支持的运动类型为running, cycling, hiking。 +3. run_data_sync.yml中的修改: + + ```yaml + RUN_TYPE: keep_to_starva_sync + ``` + +
+ ### Total Data Analysis
@@ -951,11 +1051,11 @@ python3(python) run_page/gen_svg.py --from-db --type circular --use-localtime 4. 为 GitHub Actions 添加代码提交权限,访问仓库的 `Settings > Actions > General`页面,找到 `Workflow permissions` 的设置项,将选项配置为 `Read and write permissions`,支持 CI 将运动数据更新后提交到仓库中。 -5. 如果想把你的 running_page 部署在 xxx.github.io 而不是 xxx.github.io/run_page,需要做三点 +5. 如果想把你的 running_page 部署在 xxx.github.io 而不是 xxx.github.io/run_page 亦或是想要添加自定义域名于 GitHub Pages,需要做三点 - 修改你的 fork 的 running_page 仓库改名为 xxx.github.io, xxx 是你 github 的 username - 修改 gh-pages.yml 中的 Build 模块,删除 `${{ github.event.repository.name }}` 改为`run: PATH_PREFIX=/ pnpm build` 即可 -- src/static/site-metadata.ts 中 `siteUrl: ''` 即可 +- 修改 src/static/site-metadata.ts 中 `siteUrl: ''` 或是添加你的自定义域名,`siteUrl: '[your_own_domain]'`, 即可
@@ -1100,7 +1200,7 @@ curl https://api.github.com/repos/yihong0618/running_page/actions/workflows -H " - vercel git - 如果想 ignpre gh-pages 可以在 `settings` -> `build` -> `Ignored Build Step` -> `Custom` 输入命令: + 如果想 ignore gh-pages 可以在 `settings` -> `build` -> `Ignored Build Step` -> `Custom` 输入命令: ```bash if [ "$VERCEL_GIT_COMMIT_REF" != "gh-pages" ]; then exit 1; else exit 0; diff --git a/README.md b/README.md index faeeb702ca9..40ffc1bc26d 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ English | [简体中文](https://github.com/yihong0618/running_page/blob/master/ | [zhubao315](https://github.com/zhubao315) | | Strava | | [shaonianche](https://github.com/shaonianche) | | Strava | | [yihong0618](https://github.com/yihong0618) | | Nike | -| [superleeyom](https://github.com/superleeyom) | | Nike | +| [superleeyom](https://github.com/superleeyom) | | Strava | | [geekplux](https://github.com/geekplux) | | Nike | | [guanlan](https://github.com/guanlan) | | Strava | | [tuzimoe](https://github.com/tuzimoe) | | Nike | @@ -93,7 +93,13 @@ English | [简体中文](https://github.com/yihong0618/running_page/blob/master/ | [s1smart](https://github.com/s1smart) | | Strava | | [XmchxUp](https://github.com/XmchxUp) | | Strava | | [Ryan](https://github.com/85Ryan) | | Strava | -| [PPZ](https://github.com/8824PPZ) | | Strava | +| [PPZ](https://github.com/8824PPZ) | | Strava | +| [Yer1k](https://github.com/Yer1k) | | Strava | +| [AlienVision](https://github.com/weaming) | | Strava | +| [闻笑忘](https://wenxiaowan.com) | | 苹果健身 | +| [Vensent](https://github.com/Vensent) | | Garmin | +| [Zeonsing](https://github.com/NoonieBao) | | Coros | +| [yaoper](https://github.com/yaoper) | | codoon | ## How it works @@ -124,10 +130,11 @@ English | [简体中文](https://github.com/yihong0618/running_page/blob/master/ - **[Garmin-CN_to_Garmin(Sync Garmin-CN activities to Garmin Global)](#garmin-cn-to-garmin)** - **[Nike_to_Strava(Using NRC Run, Strava backup data)](#nike_to_strava)** - **[Tcx_to_Strava(upload all tcx data to strava)](#tcx_to_strava)** +- **[Tcx_to_Garmin(upload all tcx data to Garmin)](#tcx_to_garmin)** - **[Gpx_to_Strava(upload all gpx data to strava)](#gpx_to_strava)** - **[Garmin_to_Strava(Using Garmin Run, Strava backup data)](#garmin_to_strava)** - **[Strava_to_Garmin(Using Strava Run, Garmin backup data)](#strava_to_garmin)** - +- **[Coros](#Coros)** ## Download Clone or fork the repo. @@ -165,6 +172,9 @@ docker build -t running_page:latest . --build-arg app=Strava --build-arg client_ # Nike_to_Strava docker build -t running_page:latest . --build-arg app=Nike_to_Strava --build-arg nike_refresh_token="" --build-arg client_id="" --build-arg client_secret="" --build-arg refresh_token="" +# Keep +docker build -t running_page:latest . --build-arg app=Keep --build-arg keep_phone_number="" --build-arg keep_password="" + # run docker run -itd -p 80:80 running_page:latest @@ -215,6 +225,11 @@ siteMetadata: { const USE_DASH_LINE = true; // styling: route line opacity: [0, 1] const LINE_OPACITY = 0.4; +// styling: set to `true` if you want to display only the routes without showing the map +// Note: This config only affects the page display; please refer to "privacy protection" below for data protection +const PRIVACY_MODE = false; +// styling: set to `false` if you want to make light off as default, only effect when `PRIVACY_MODE` = false +const LIGHTS_ON = true; ``` - To use Google Analytics, you need to modify the configuration in the `src/utils/const.ts` file. @@ -573,6 +588,33 @@ python3(python) run_page/tcx_to_strava_sync.py xxx xxx xxx --all +### TCX_to_Garmin + +
+upload all tcx files to garmin + +
+ +1. follow the garmin steps +2. copy all your tcx files to TCX_OUT +3. Execute in the root directory: + +```bash +python3 run_page/tcx_to_garmin_sync.py ${{ secrets.GARMIN_SECRET_STRING_CN }} --is-cn +``` + +example: + +```bash +python run_page/tcx_to_garmin_sync.py xxx --is-cn +or Garmin Global +python run_page/tcx_to_garmin_sync.py xxx +``` + +4. if you want to all files add args `--all` + +
+ ### GPX_to_Strava
@@ -678,6 +720,29 @@ ps: **when initializing for the first time, if you have a large amount of strava
+ + +### Coros + +
+Get your Coros data + +#### Enter the following command in the terminal + +```bash +python run_page/coros_sync.py 'your coros account' 'your coros password' +``` + +#### Modify `run_data_sync.yml` env.RUN_TYPE: _coros_ + +#### Set the Coros account information in github action + +- configure the `COROS_ACCOUNT` , `COROS_PASSWORD` + + ![github-action](https://img3.uploadhouse.com/fileuploads/30980/3098042335f8995623f8b50776c4fad4cf7fff8d.png) + +
+ ### Total Data Analysis
@@ -770,11 +835,11 @@ For more display effects, see: 4. make sure you have write permissions in Workflow permissions settings. -5. If you want to deploy your running_page to xxx.github.io instead of xxx.github.io/running_page, you need to do three things: +5. If you want to deploy your running_page to xxx.github.io instead of xxx.github.io/running_page or redirect your GitHub Pages to a custom domain, you need to do three things: - Rename your forked running_page repository to `xxx.github.io`, where xxx is your GitHub username - Modify the Build module in gh-pages.yml, remove `${{ github.event.repository.name }}` and change to `run: PATH_PREFIX=/ pnpm build` -- In `src/static/site-metadata.ts`, set siteUrl: '' +- In `src/static/site-metadata.ts`, set siteUrl: '' or your custom domain URL
@@ -918,7 +983,7 @@ Just enjoy it~ Strava API Rate Limit Timeout. Retry in 799.491622 seconds ``` -- vercel git ignpre gh-pages: +- vercel git ignore gh-pages: you can change settings -> build -> Ignored Build Step -> Custom command diff --git a/assets/github_2024.svg b/assets/github_2024.svg new file mode 100644 index 00000000000..441af32ea60 --- /dev/null +++ b/assets/github_2024.svg @@ -0,0 +1,2 @@ + +2024 RunningATHLETEyihong0618STATISTICSNumber: 18Weekly: 9.0Total: 45.0 kmAvg: 2.5 kmMin: 1.2 kmMax: 5.3 km202445.0 kmJanFebMarAprMayJunJulAugSepOctNovDec2024-01-01 2.4 km2024-01-02 3.8 km2024-01-03 3.3 km2024-01-04 4.7 km2024-01-05 2.9 km2024-01-06 3.1 km2024-01-07 2.8 km2024-01-08 3.3 km2024-01-09 3.7 km2024-01-10 3.9 km2024-01-11 2.5 km2024-01-12 3.3 km2024-01-13 5.3 km2024-01-142024-01-152024-01-162024-01-172024-01-182024-01-192024-01-202024-01-212024-01-222024-01-232024-01-242024-01-252024-01-262024-01-272024-01-282024-01-292024-01-302024-01-312024-02-012024-02-022024-02-032024-02-042024-02-052024-02-062024-02-072024-02-082024-02-092024-02-102024-02-112024-02-122024-02-132024-02-142024-02-152024-02-162024-02-172024-02-182024-02-192024-02-202024-02-212024-02-222024-02-232024-02-242024-02-252024-02-262024-02-272024-02-282024-02-292024-03-012024-03-022024-03-032024-03-042024-03-052024-03-062024-03-072024-03-082024-03-092024-03-102024-03-112024-03-122024-03-132024-03-142024-03-152024-03-162024-03-172024-03-182024-03-192024-03-202024-03-212024-03-222024-03-232024-03-242024-03-252024-03-262024-03-272024-03-282024-03-292024-03-302024-03-312024-04-012024-04-022024-04-032024-04-042024-04-052024-04-062024-04-072024-04-082024-04-092024-04-102024-04-112024-04-122024-04-132024-04-142024-04-152024-04-162024-04-172024-04-182024-04-192024-04-202024-04-212024-04-222024-04-232024-04-242024-04-252024-04-262024-04-272024-04-282024-04-292024-04-302024-05-012024-05-022024-05-032024-05-042024-05-052024-05-062024-05-072024-05-082024-05-092024-05-102024-05-112024-05-122024-05-132024-05-142024-05-152024-05-162024-05-172024-05-182024-05-192024-05-202024-05-212024-05-222024-05-232024-05-242024-05-252024-05-262024-05-272024-05-282024-05-292024-05-302024-05-312024-06-012024-06-022024-06-032024-06-042024-06-052024-06-062024-06-072024-06-082024-06-092024-06-102024-06-112024-06-122024-06-132024-06-142024-06-152024-06-162024-06-172024-06-182024-06-192024-06-202024-06-212024-06-222024-06-232024-06-242024-06-252024-06-262024-06-272024-06-282024-06-292024-06-302024-07-012024-07-022024-07-032024-07-042024-07-052024-07-062024-07-072024-07-082024-07-092024-07-102024-07-112024-07-122024-07-132024-07-142024-07-152024-07-162024-07-172024-07-182024-07-192024-07-202024-07-212024-07-222024-07-232024-07-242024-07-252024-07-262024-07-272024-07-282024-07-292024-07-302024-07-312024-08-012024-08-022024-08-032024-08-042024-08-052024-08-062024-08-072024-08-082024-08-092024-08-102024-08-112024-08-122024-08-132024-08-142024-08-152024-08-162024-08-172024-08-182024-08-192024-08-202024-08-212024-08-222024-08-232024-08-242024-08-252024-08-262024-08-272024-08-282024-08-292024-08-302024-08-312024-09-012024-09-022024-09-032024-09-042024-09-052024-09-062024-09-072024-09-082024-09-092024-09-102024-09-112024-09-122024-09-132024-09-142024-09-152024-09-162024-09-172024-09-182024-09-192024-09-202024-09-212024-09-222024-09-232024-09-242024-09-252024-09-262024-09-272024-09-282024-09-292024-09-302024-10-012024-10-022024-10-032024-10-042024-10-052024-10-062024-10-072024-10-082024-10-092024-10-102024-10-112024-10-122024-10-132024-10-142024-10-152024-10-162024-10-172024-10-182024-10-192024-10-202024-10-212024-10-222024-10-232024-10-242024-10-252024-10-262024-10-272024-10-282024-10-292024-10-302024-10-312024-11-012024-11-022024-11-032024-11-042024-11-052024-11-062024-11-072024-11-082024-11-092024-11-102024-11-112024-11-122024-11-132024-11-142024-11-152024-11-162024-11-172024-11-182024-11-192024-11-202024-11-212024-11-222024-11-232024-11-242024-11-252024-11-262024-11-272024-11-282024-11-292024-11-302024-12-012024-12-022024-12-032024-12-042024-12-052024-12-062024-12-072024-12-082024-12-092024-12-102024-12-112024-12-122024-12-132024-12-142024-12-152024-12-162024-12-172024-12-182024-12-192024-12-202024-12-212024-12-222024-12-232024-12-242024-12-252024-12-262024-12-272024-12-282024-12-292024-12-302024-12-31 \ No newline at end of file diff --git a/assets/year_2024.svg b/assets/year_2024.svg new file mode 100644 index 00000000000..18ca7218da7 --- /dev/null +++ b/assets/year_2024.svg @@ -0,0 +1,2 @@ + +2024JanuaryFebruaryMarchAprilMayJuneJulyAugustSeptemberOctoberNovemberDecember \ No newline at end of file diff --git a/package.json b/package.json index 054204c055a..f49ea588920 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dependencies": { "@mapbox/mapbox-gl-language": "^1.0.0", "@mapbox/polyline": "^1.1.1", + "@surbowl/world-geo-json-zh": "^2.1.3", "@svgr/plugin-svgo": "^8.1.0", "@vercel/analytics": "^0.1.6", "@vitejs/plugin-react": "^4.0.0", @@ -19,10 +20,6 @@ "react-helmet-async": "^1.3.0", "react-map-gl": "^7.1.6", "react-router-dom": "^6.15.0", - "sass": "^1.52.3", - "sass-mq": "^6.0.0", - "tachyons": "^4.12.0", - "tachyons-sass": "git+https://github.com/tachyons-css/tachyons-sass.git", "viewport-mercator-project": "^7.0.4", "vite": "^4.3.9", "vite-plugin-svgr": "^3.2.0", @@ -30,6 +27,7 @@ }, "license": "MIT", "private": true, + "type": "module", "scripts": { "data:clean": "rm run_page/data.db {GPX,TCX,FIT}_OUT/* activities/* src/static/activities.json", "data:download:garmin": "python3 run_page/garmin_sync.py", @@ -38,7 +36,7 @@ "develop": "vite dev", "serve": "vite serve", "lint": "eslint --ext .ts,.tsx src --fix", - "check": "prettier --write src/main.tsx src/**/*.{scss,json,ts,tsx} *.{md,yaml,json,ts,js}", + "check": "prettier --write src/main.tsx src/**/*.{css,json,ts,tsx} *.{md,yaml,json,ts,js}", "ci": "pnpm run check && pnpm run lint && pnpm run build" }, "engineStrict": true, @@ -51,17 +49,22 @@ "url": "https://github.com/yihong0618/running_page" }, "devDependencies": { + "@types/geojson": "^7946.0.14", "@types/mapbox__polyline": "^1.0.2", "@types/node": "^20.3.3", + "@types/prop-types": "^15.7.11", "@types/react": "^18.2.14", "@types/react-dom": "^18.2.6", "@typescript-eslint/parser": "^5.59.2", + "autoprefixer": "^10.4.19", "eslint": "^8.17.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", - "prettier": "2.8.8", + "postcss": "^8.4.38", + "prettier": "^3.2.5", + "prettier-plugin-tailwindcss": "^0.5.13", + "tailwindcss": "^3.4.3", "typescript": "^5.1.6" - }, - "packageManager": "pnpm@8.7.0" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c5483dbf78..597f6795685 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,12 @@ dependencies: '@mapbox/polyline': specifier: ^1.1.1 version: 1.2.0 + '@surbowl/world-geo-json-zh': + specifier: ^2.1.3 + version: 2.1.3 '@svgr/plugin-svgo': specifier: ^8.1.0 - version: 8.1.0(@svgr/core@7.0.0) + version: 8.1.0(@svgr/core@8.1.0) '@vercel/analytics': specifier: ^0.1.6 version: 0.1.11(react@18.2.0) @@ -50,24 +53,12 @@ dependencies: react-router-dom: specifier: ^6.15.0 version: 6.15.0(react-dom@18.2.0)(react@18.2.0) - sass: - specifier: ^1.52.3 - version: 1.66.1 - sass-mq: - specifier: ^6.0.0 - version: 6.0.0 - tachyons: - specifier: ^4.12.0 - version: 4.12.0 - tachyons-sass: - specifier: git+https://github.com/tachyons-css/tachyons-sass.git - version: github.com/tachyons-css/tachyons-sass/2dce89b83729bddb9f4498c7d6f82b73d60d6538 viewport-mercator-project: specifier: ^7.0.4 version: 7.0.4 vite: specifier: ^4.3.9 - version: 4.4.9(@types/node@20.5.7)(sass@1.66.1) + version: 4.4.9(@types/node@20.5.7) vite-plugin-svgr: specifier: ^3.2.0 version: 3.2.0(vite@4.4.9) @@ -76,12 +67,18 @@ dependencies: version: 4.2.0(typescript@5.2.2)(vite@4.4.9) devDependencies: + '@types/geojson': + specifier: ^7946.0.14 + version: 7946.0.14 '@types/mapbox__polyline': specifier: ^1.0.2 version: 1.0.2 '@types/node': specifier: ^20.3.3 version: 20.5.7 + '@types/prop-types': + specifier: ^15.7.11 + version: 15.7.11 '@types/react': specifier: ^18.2.14 version: 18.2.21 @@ -91,6 +88,9 @@ devDependencies: '@typescript-eslint/parser': specifier: ^5.59.2 version: 5.62.0(eslint@8.48.0)(typescript@5.2.2) + autoprefixer: + specifier: ^10.4.19 + version: 10.4.19(postcss@8.4.38) eslint: specifier: ^8.17.0 version: 8.48.0 @@ -99,13 +99,22 @@ devDependencies: version: 8.10.0(eslint@8.48.0) eslint-plugin-prettier: specifier: ^4.2.1 - version: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.48.0)(prettier@2.8.8) + version: 4.2.1(eslint-config-prettier@8.10.0)(eslint@8.48.0)(prettier@3.2.5) eslint-plugin-react: specifier: ^7.32.2 version: 7.33.2(eslint@8.48.0) + postcss: + specifier: ^8.4.38 + version: 8.4.38 prettier: - specifier: 2.8.8 - version: 2.8.8 + specifier: ^3.2.5 + version: 3.2.5 + prettier-plugin-tailwindcss: + specifier: ^0.5.13 + version: 0.5.13(prettier@3.2.5) + tailwindcss: + specifier: ^3.4.3 + version: 3.4.3 typescript: specifier: ^5.1.6 version: 5.2.2 @@ -117,6 +126,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -125,6 +139,14 @@ packages: '@jridgewell/trace-mapping': 0.3.19 dev: false + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: false + /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -133,11 +155,24 @@ packages: chalk: 2.4.2 dev: false + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.2 + picocolors: 1.0.0 + dev: false + /@babel/compat-data@7.22.9: resolution: {integrity: sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==} engines: {node: '>=6.9.0'} dev: false + /@babel/compat-data@7.24.4: + resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/core@7.22.11: resolution: {integrity: sha512-lh7RJrtPdhibbxndr6/xx0w8+CVlY5FJZiaSz908Fpy+G0xkBFTvwLcKJFF4PJxVfGhVWNebikpWGnOoC71juQ==} engines: {node: '>=6.9.0'} @@ -161,6 +196,29 @@ packages: - supports-color dev: false + /@babel/core@7.24.4: + resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) + '@babel/helpers': 7.24.4 + '@babel/parser': 7.24.4 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/generator@7.22.10: resolution: {integrity: sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==} engines: {node: '>=6.9.0'} @@ -171,6 +229,16 @@ packages: jsesc: 2.5.2 dev: false + /@babel/generator@7.24.4: + resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: false + /@babel/helper-compilation-targets@7.22.10: resolution: {integrity: sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==} engines: {node: '>=6.9.0'} @@ -182,6 +250,22 @@ packages: semver: 6.3.1 dev: false + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.4 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: false + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/helper-environment-visitor@7.22.5: resolution: {integrity: sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==} engines: {node: '>=6.9.0'} @@ -195,6 +279,14 @@ packages: '@babel/types': 7.22.11 dev: false + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/types': 7.24.0 + dev: false + /@babel/helper-hoist-variables@7.22.5: resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} engines: {node: '>=6.9.0'} @@ -209,6 +301,13 @@ packages: '@babel/types': 7.22.11 dev: false + /@babel/helper-module-imports@7.24.3: + resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + dev: false + /@babel/helper-module-transforms@7.22.9(@babel/core@7.22.11): resolution: {integrity: sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==} engines: {node: '>=6.9.0'} @@ -223,6 +322,20 @@ packages: '@babel/helper-validator-identifier': 7.22.5 dev: false + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.24.3 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: false + /@babel/helper-plugin-utils@7.22.5: resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} engines: {node: '>=6.9.0'} @@ -247,6 +360,16 @@ packages: engines: {node: '>=6.9.0'} dev: false + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/helper-validator-identifier@7.22.5: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} @@ -257,6 +380,11 @@ packages: engines: {node: '>=6.9.0'} dev: false + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/helpers@7.22.11: resolution: {integrity: sha512-vyOXC8PBWaGc5h7GMsNx68OH33cypkEDJCHvYVVgVbbxJDROYVtexSk0gK5iCF1xNjRIN2s8ai7hwkWDq5szWg==} engines: {node: '>=6.9.0'} @@ -268,6 +396,17 @@ packages: - supports-color dev: false + /@babel/helpers@7.24.4: + resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/highlight@7.22.13: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} engines: {node: '>=6.9.0'} @@ -277,6 +416,16 @@ packages: js-tokens: 4.0.0 dev: false + /@babel/highlight@7.24.2: + resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + dev: false + /@babel/parser@7.22.13: resolution: {integrity: sha512-3l6+4YOvc9wx7VlCSw4yQfcBo01ECA8TicQfbnCPuCEpRQrf+gTUyGdxNw+pyTUyywp6JRD1w0YQs9TpBXYlkw==} engines: {node: '>=6.0.0'} @@ -285,23 +434,31 @@ packages: '@babel/types': 7.22.11 dev: false - /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.22.11): + /@babel/parser@7.24.4: + resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + dev: false + + /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.24.4): resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.11 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.22.5 dev: false - /@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.22.11): + /@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.24.4): resolution: {integrity: sha512-yIiRO6yobeEIaI0RTbIr8iAK9FcBHLtZq0S89ZPjDLQXBA4xvghaKqI0etp/tF3htTM0sazJKKLz9oEiGRtu7w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.22.11 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.22.5 dev: false @@ -321,6 +478,15 @@ packages: '@babel/types': 7.22.11 dev: false + /@babel/template@7.24.0: + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 + dev: false + /@babel/traverse@7.22.11: resolution: {integrity: sha512-mzAenteTfomcB7mfPtyi+4oe5BZ6MXxWcn4CX+h4IRJ+OOGXBrWU6jDQavkQI9Vuc5P+donFabBfFCcmWka9lQ==} engines: {node: '>=6.9.0'} @@ -339,6 +505,24 @@ packages: - supports-color dev: false + /@babel/traverse@7.24.1: + resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: false + /@babel/types@7.22.11: resolution: {integrity: sha512-siazHiGuZRz9aB9NpHy9GOs9xiQPKnMzgdr493iI1M67vRXpnEq8ZOOKzezC5q7zwuQ6sDhdSp4SD9ixKSqKZg==} engines: {node: '>=6.9.0'} @@ -348,6 +532,15 @@ packages: to-fast-properties: 2.0.0 dev: false + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: false + /@esbuild/android-arm64@0.18.20: resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -603,6 +796,18 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -612,19 +817,34 @@ packages: '@jridgewell/trace-mapping': 0.3.19 dev: false + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + /@jridgewell/resolve-uri@3.1.1: resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} engines: {node: '>=6.0.0'} dev: false + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} dev: false + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: false /@jridgewell/trace-mapping@0.3.19: resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} @@ -633,6 +853,12 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: false + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + /@mapbox/geojson-rewind@0.5.2: resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} hasBin: true @@ -728,6 +954,13 @@ packages: fastq: 1.15.0 dev: true + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@remix-run/router@1.8.0: resolution: {integrity: sha512-mrfKqIHnSZRyIzBcanNJmVQELTnX+qagEDlcKO90RgRBVOZGSGvZKeDihTRfWcqoDn5N/NkUcwWTccnpN18Tfg==} engines: {node: '>=14.0.0'} @@ -747,6 +980,10 @@ packages: picomatch: 2.3.1 dev: false + /@surbowl/world-geo-json-zh@2.1.3: + resolution: {integrity: sha512-6m/eVcSsWvFXYkaEQLHCxJqFSkTMVaDgfTg9weQr2lozyrxxH+SsRTq24DFfCqFX7L9zKtDQcP6+5VKemY1Rcg==} + dev: false + /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==} engines: {node: '>=14'} @@ -756,6 +993,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==} engines: {node: '>=14'} @@ -765,6 +1011,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==} engines: {node: '>=14'} @@ -774,6 +1029,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==} engines: {node: '>=14'} @@ -783,6 +1047,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==} engines: {node: '>=14'} @@ -792,6 +1065,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==} engines: {node: '>=14'} @@ -801,6 +1083,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==} engines: {node: '>=14'} @@ -810,6 +1101,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.24.4): + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==} engines: {node: '>=12'} @@ -819,6 +1119,15 @@ packages: '@babel/core': 7.22.11 dev: false + /@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.24.4): + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: false + /@svgr/babel-preset@7.0.0(@babel/core@7.22.11): resolution: {integrity: sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==} engines: {node: '>=14'} @@ -836,6 +1145,23 @@ packages: '@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.22.11) dev: false + /@svgr/babel-preset@8.1.0(@babel/core@7.24.4): + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.24.4) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.24.4) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.24.4) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.24.4) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.24.4) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.24.4) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.24.4) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.24.4) + dev: false + /@svgr/core@7.0.0: resolution: {integrity: sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==} engines: {node: '>=14'} @@ -848,6 +1174,20 @@ packages: - supports-color dev: false + /@svgr/core@8.1.0(typescript@5.2.2): + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.4 + '@svgr/babel-preset': 8.1.0(@babel/core@7.24.4) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.2.2) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + /@svgr/hast-util-to-babel-ast@7.0.0: resolution: {integrity: sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==} engines: {node: '>=14'} @@ -868,13 +1208,13 @@ packages: - supports-color dev: false - /@svgr/plugin-svgo@8.1.0(@svgr/core@7.0.0): + /@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0): resolution: {integrity: sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==} engines: {node: '>=14'} peerDependencies: '@svgr/core': '*' dependencies: - '@svgr/core': 7.0.0 + '@svgr/core': 8.1.0(typescript@5.2.2) cosmiconfig: 8.2.0 deepmerge: 4.3.1 svgo: 3.1.0 @@ -889,19 +1229,19 @@ packages: resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} dev: false - /@types/geojson@7946.0.10: - resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==} + /@types/geojson@7946.0.14: + resolution: {integrity: sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==} /@types/mapbox-gl@2.7.13: resolution: {integrity: sha512-qNffhTdYkeFl8QG9Q1zPPJmcs8PvHgmLa1PcwP1rxvcfMsIgcFr/FnrCttG0ZnH7Kzdd7xfECSRNTWSr4jC3PQ==} dependencies: - '@types/geojson': 7946.0.10 + '@types/geojson': 7946.0.14 dev: false /@types/mapbox__polyline@1.0.2: resolution: {integrity: sha512-Kr/oznVL3e8uvWM0+VPlVu2rVP+jKyVW/994HE9YJFBfcAcKqB4wMvqCdgXLG0SSrxxlKc5Nqaj7zt+KRl9oCA==} dependencies: - '@types/geojson': 7946.0.10 + '@types/geojson': 7946.0.14 dev: true /@types/minimist@1.2.2: @@ -915,8 +1255,8 @@ packages: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} dev: false - /@types/prop-types@15.7.5: - resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} dev: true /@types/react-dom@18.2.7: @@ -928,7 +1268,7 @@ packages: /@types/react@18.2.21: resolution: {integrity: sha512-neFKG/sBAwGxHgXiIxnbm3/AAVQ/cMRS93hvBpg8xYRbeQSPVABp9U2bRnPf0iI4+Ucdv3plSxKK+3CW2ENJxA==} dependencies: - '@types/prop-types': 15.7.5 + '@types/prop-types': 15.7.11 '@types/scheduler': 0.16.3 csstype: 3.1.2 dev: true @@ -1013,11 +1353,11 @@ packages: peerDependencies: vite: ^4.2.0 dependencies: - '@babel/core': 7.22.11 - '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.11) - '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.11) + '@babel/core': 7.24.4 + '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.24.4) react-refresh: 0.14.0 - vite: 4.4.9(@types/node@20.5.7)(sass@1.66.1) + vite: 4.4.9(@types/node@20.5.7) transitivePeerDependencies: - supports-color dev: false @@ -1050,6 +1390,11 @@ packages: engines: {node: '>=8'} dev: true + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1064,13 +1409,26 @@ packages: color-convert: 2.0.1 dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: false + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1161,6 +1519,22 @@ packages: has-symbols: 1.0.3 dev: true + /autoprefixer@10.4.19(postcss@8.4.38): + resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001605 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + dev: true + /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} @@ -1173,7 +1547,7 @@ packages: /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - dev: false + dev: true /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -1186,23 +1560,40 @@ packages: concat-map: 0.0.1 dev: true + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} engines: {node: '>=8'} dependencies: fill-range: 7.0.1 + dev: true /browserslist@4.21.10: resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true dependencies: - caniuse-lite: 1.0.30001524 + caniuse-lite: 1.0.30001605 electron-to-chromium: 1.4.505 node-releases: 2.0.13 update-browserslist-db: 1.0.11(browserslist@4.21.10) dev: false + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001605 + electron-to-chromium: 1.4.726 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) + /bytewise-core@1.2.3: resolution: {integrity: sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==} dependencies: @@ -1227,6 +1618,11 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + /camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} @@ -1246,9 +1642,8 @@ packages: engines: {node: '>=10'} dev: false - /caniuse-lite@1.0.30001524: - resolution: {integrity: sha512-Jj917pJtYg9HSJBF95HVX3Cdr89JUyLT4IZ8SvM5aDRni95swKgYi3TgYLH5hnGfPE/U1dg6IfZ50UsIlLkwSA==} - dev: false + /caniuse-lite@1.0.30001605: + resolution: {integrity: sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==} /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -1280,7 +1675,7 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: false + dev: true /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1303,6 +1698,11 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + /commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -1316,6 +1716,10 @@ packages: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: false + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: false + /cosmiconfig@8.2.0: resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} engines: {node: '>=14'} @@ -1326,6 +1730,22 @@ packages: path-type: 4.0.0 dev: false + /cosmiconfig@8.3.6(typescript@5.2.2): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 5.2.2 + dev: false + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1370,6 +1790,12 @@ packages: resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} dev: false + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + /csso@5.0.5: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -1422,6 +1848,10 @@ packages: object-keys: 1.1.1 dev: true + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1429,6 +1859,10 @@ packages: path-type: 4.0.0 dev: true + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -1470,14 +1904,36 @@ packages: domhandler: 5.0.3 dev: false + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: false + /earcut@2.2.4: resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} dev: false + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /electron-to-chromium@1.4.505: resolution: {integrity: sha512-0A50eL5BCCKdxig2SsCXhpuztnB9PfUgRMojj5tMvt8O54lbwz3t6wNgnpiTRosw5QjlJB7ixhVyeg8daLQwSQ==} dev: false + /electron-to-chromium@1.4.726: + resolution: {integrity: sha512-xtjfBXn53RORwkbyKvDfTajtnTp0OJoPOIBzXvkNbb7+YYvCHJflba3L7Txyx/6Fov3ov2bGPr/n5MTixmPhdQ==} + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -1612,6 +2068,10 @@ packages: engines: {node: '>=6'} dev: false + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1631,7 +2091,7 @@ packages: eslint: 8.48.0 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.48.0)(prettier@2.8.8): + /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.10.0)(eslint@8.48.0)(prettier@3.2.5): resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} engines: {node: '>=12.0.0'} peerDependencies: @@ -1644,7 +2104,7 @@ packages: dependencies: eslint: 8.48.0 eslint-config-prettier: 8.10.0(eslint@8.48.0) - prettier: 2.8.8 + prettier: 3.2.5 prettier-linter-helpers: 1.0.0 dev: true @@ -1829,6 +2289,7 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + dev: true /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} @@ -1865,6 +2326,18 @@ packages: is-callable: 1.2.7 dev: true + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -1874,7 +2347,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: false optional: true /function-bind@1.1.1: @@ -1948,6 +2420,7 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 + dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -1956,6 +2429,18 @@ packages: is-glob: 4.0.3 dev: true + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.0.4 + path-scurry: 1.10.2 + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -2077,10 +2562,6 @@ packages: engines: {node: '>= 4'} dev: true - /immutable@4.3.4: - resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} - dev: false - /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2154,7 +2635,7 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 - dev: false + dev: true /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -2196,6 +2677,7 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + dev: true /is-finalizationregistry@1.0.2: resolution: {integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==} @@ -2203,6 +2685,11 @@ packages: call-bind: 1.0.2 dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + /is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} engines: {node: '>= 0.4'} @@ -2215,6 +2702,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + dev: true /is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -2235,6 +2723,7 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + dev: true /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} @@ -2332,6 +2821,20 @@ packages: reflect.getprototypeof: 1.0.3 dev: true + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + dev: true + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2406,9 +2909,18 @@ packages: type-check: 0.4.0 dev: true + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lilconfig@3.1.1: + resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} + engines: {node: '>=14'} + dev: true + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: false /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} @@ -2434,6 +2946,17 @@ packages: dependencies: js-tokens: 4.0.0 + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.6.2 + dev: false + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + dev: true + /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} dependencies: @@ -2533,6 +3056,13 @@ packages: brace-expansion: 1.1.11 dev: true + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -2546,6 +3076,11 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: false + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -2553,20 +3088,37 @@ packages: resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} dev: false - /nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: false /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.6.2 + dev: false + /node-releases@2.0.13: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: false + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + /normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} dependencies: @@ -2579,7 +3131,12 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: false + dev: true + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true /nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -2591,6 +3148,11 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: true @@ -2728,6 +3290,14 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + dev: true + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2742,20 +3312,89 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: false /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - /postcss@8.4.29: - resolution: {integrity: sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==} + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + dev: true + + /postcss-import@15.1.0(postcss@8.4.38): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.38 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.4 + dev: true + + /postcss-js@4.0.1(postcss@8.4.38): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.38 + dev: true + + /postcss-load-config@4.0.2(postcss@8.4.38): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.1 + postcss: 8.4.38 + yaml: 2.4.1 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.38): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.38 + postcss-selector-parser: 6.0.16 + dev: true + + /postcss-selector-parser@6.0.16: + resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.6 + nanoid: 3.3.7 picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: false + source-map-js: 1.2.0 /potpack@2.0.0: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} @@ -2773,9 +3412,64 @@ packages: fast-diff: 1.3.0 dev: true - /prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} + /prettier-plugin-tailwindcss@0.5.13(prettier@3.2.5): + resolution: {integrity: sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig-melody': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig-melody': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + dependencies: + prettier: 3.2.5 + dev: true + + /prettier@3.2.5: + resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} + engines: {node: '>=14'} hasBin: true dev: true @@ -2899,6 +3593,12 @@ packages: loose-envify: 1.4.0 dev: false + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -2923,7 +3623,7 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 - dev: false + dev: true /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} @@ -2975,7 +3675,6 @@ packages: is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: false /resolve@2.0.0-next.4: resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} @@ -3034,20 +3733,6 @@ packages: is-regex: 1.1.4 dev: true - /sass-mq@6.0.0: - resolution: {integrity: sha512-h4VicIy8lszFlqqggqLIFGt/9wS5fHLPoTXHRjC8Vw6UsA4s4JtDvEeypXbbECfgY336mXyc/cdpbRacH0UzGA==} - dev: false - - /sass@1.66.1: - resolution: {integrity: sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==} - engines: {node: '>=14.0.0'} - hasBin: true - dependencies: - chokidar: 3.5.3 - immutable: 4.3.4 - source-map-js: 1.0.2 - dev: false - /scheduler@0.23.0: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: @@ -3105,11 +3790,23 @@ packages: object-inspect: 1.12.3 dev: true + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: false + /sort-asc@0.2.0: resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} engines: {node: '>=0.10.0'} @@ -3137,6 +3834,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + /spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} dependencies: @@ -3166,6 +3867,24 @@ packages: extend-shallow: 3.0.2 dev: false + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + /string.prototype.matchall@4.0.9: resolution: {integrity: sha512-6i5hL3MqG/K2G43mWXWgP+qizFW/QH/7kCNN13JrJS5q48FN5IKksLDscexKP3dnmB6cdm9jlNgAsWNLpSykmA==} dependencies: @@ -3211,6 +3930,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -3223,6 +3949,20 @@ packages: engines: {node: '>=8'} dev: true + /sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.3.12 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + dev: true + /supercluster@8.0.1: resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} dependencies: @@ -3265,18 +4005,54 @@ packages: picocolors: 1.0.0 dev: false - /tachyons-custom@4.9.8: - resolution: {integrity: sha512-MVnslsN5dFswmh2+Sw+OOA4/6pQfnJrA8/wrtaf+Px347wze3MIk3lp3Q3QmbUPqrJda79s0dZtY3dgTwVHdfg==} - dev: false - - /tachyons@4.12.0: - resolution: {integrity: sha512-2nA2IrYFy3raCM9fxJ2KODRGHVSZNTW3BR0YnlGsLUf1DA3pk3YfWZ/DdfbnZK6zLZS+jUenlUGJsKcA5fUiZg==} - dev: false + /tailwindcss@3.4.3: + resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.1 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.0 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.38 + postcss-import: 15.1.0(postcss@8.4.38) + postcss-js: 4.0.1(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-nested: 6.0.1(postcss@8.4.38) + postcss-selector-parser: 6.0.16 + resolve: 1.22.4 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + dev: true /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + /tinyqueue@2.0.3: resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} dev: false @@ -3291,12 +4067,17 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + dev: true /trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} dev: false + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + /tsconfck@2.1.2(typescript@5.2.2): resolution: {integrity: sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==} engines: {node: ^14.13.1 || ^16 || >=18} @@ -3314,6 +4095,10 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsutils@3.21.0(typescript@5.2.2): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -3434,12 +4219,26 @@ packages: picocolors: 1.0.0 dev: false + /update-browserslist-db@1.0.13(browserslist@4.23.0): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.0 + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.0 dev: true + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -3461,7 +4260,7 @@ packages: '@rollup/pluginutils': 5.0.4 '@svgr/core': 7.0.0 '@svgr/plugin-jsx': 7.0.0 - vite: 4.4.9(@types/node@20.5.7)(sass@1.66.1) + vite: 4.4.9(@types/node@20.5.7) transitivePeerDependencies: - rollup - supports-color @@ -3478,13 +4277,13 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.2.2) - vite: 4.4.9(@types/node@20.5.7)(sass@1.66.1) + vite: 4.4.9(@types/node@20.5.7) transitivePeerDependencies: - supports-color - typescript dev: false - /vite@4.4.9(@types/node@20.5.7)(sass@1.66.1): + /vite@4.4.9(@types/node@20.5.7): resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -3514,9 +4313,8 @@ packages: dependencies: '@types/node': 20.5.7 esbuild: 0.18.20 - postcss: 8.4.29 + postcss: 8.4.38 rollup: 3.28.1 - sass: 1.66.1 optionalDependencies: fsevents: 2.3.3 dev: false @@ -3585,6 +4383,24 @@ packages: isexe: 2.0.0 dev: true + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true @@ -3597,6 +4413,12 @@ packages: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true + /yaml@2.4.1: + resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} + engines: {node: '>= 14'} + hasBin: true + dev: true + /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -3609,11 +4431,3 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true - - github.com/tachyons-css/tachyons-sass/2dce89b83729bddb9f4498c7d6f82b73d60d6538: - resolution: {tarball: https://codeload.github.com/tachyons-css/tachyons-sass/tar.gz/2dce89b83729bddb9f4498c7d6f82b73d60d6538} - name: tachyons-sass - version: 4.9.5 - dependencies: - tachyons-custom: 4.9.8 - dev: false diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000000..2aa7205d4b4 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/requirements.txt b/requirements.txt index bd368b63a8e..124aa41936d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ httpx>=0.15.0,<=0.15.5 gpxpy==1.4.2 -stravalib +stravalib==0.10.4 appdirs>=1.4.0 svgwrite>=1.1.9 colour>=0.1.5 @@ -14,7 +14,7 @@ timezonefinder; platform_system == "Windows" pyyaml aiofiles cloudscraper==1.2.58 -git+https://github.com/alenrajsp/tcxreader.git +tcxreader rich lxml==4.9.4 eviltransform @@ -27,4 +27,3 @@ garmin-fit-sdk haversine==2.8.0 garth pycryptodome - diff --git a/run_page/codoon_sync.py b/run_page/codoon_sync.py index 84a19bf4650..c91a69e6169 100755 --- a/run_page/codoon_sync.py +++ b/run_page/codoon_sync.py @@ -9,6 +9,7 @@ import xml.etree.ElementTree as ET from collections import namedtuple from datetime import datetime, timedelta +from xml.dom import minidom import eviltransform import gpxpy @@ -45,6 +46,8 @@ # device info user_agent = "CodoonSport(8.9.0 1170;Android 7;Sony XZ1)" did = "24-00000000-03e1-7dd7-0033-c5870033c588" +# May be Forerunner 945? +CONNECT_API_PART_NUMBER = "006-D2449-00" # fixed params base_url = "https://api.codoon.com" @@ -61,9 +64,9 @@ # for tcx type TCX_TYPE_DICT = { - 0: "Hike", + 0: "Hiking", 1: "Running", - 2: "Ride", + 2: "Biking", } # only for running sports, if you want others, please change the True to False @@ -127,6 +130,9 @@ def formated_input( def tcx_output(fit_array, run_data): + """ + If you want to make a more detailed tcx file, please refer to oppo_sync.py + """ # route ID fit_id = str(run_data["id"]) # local time @@ -149,7 +155,7 @@ def tcx_output(fit_array, run_data): }, ) # xml tree - tree = ET.ElementTree(training_center_database) + ET.ElementTree(training_center_database) # Activities activities = ET.Element("Activities") training_center_database.append(activities) @@ -163,12 +169,15 @@ def tcx_output(fit_array, run_data): activity_id.text = fit_start_time # Codoon use start_time as ID activity.append(activity_id) # Creator - activity_creator = ET.Element("Creator") + activity_creator = ET.Element("Creator", {"xsi:type": "Device_t"}) activity.append(activity_creator) # Name activity_creator_name = ET.Element("Name") - activity_creator_name.text = "咕咚" + activity_creator_name.text = "Codoon" activity_creator.append(activity_creator_name) + activity_creator_product = ET.Element("ProductID") + activity_creator_product.text = "3441" + activity_creator.append(activity_creator_product) # Lap activity_lap = ET.Element("Lap", {"StartTime": fit_start_time}) activity.append(activity_lap) @@ -215,11 +224,22 @@ def tcx_output(fit_array, run_data): altitude_meters = ET.Element("AltitudeMeters") altitude_meters.text = bytes.decode(i["elevation"]) tp.append(altitude_meters) - + # Author + author = ET.Element("Author", {"xsi:type": "Application_t"}) + training_center_database.append(author) + author_name = ET.Element("Name") + author_name.text = "Connect Api" + author.append(author_name) + author_lang = ET.Element("LangID") + author_lang.text = "en" + author.append(author_lang) + author_part = ET.Element("PartNumber") + author_part.text = CONNECT_API_PART_NUMBER + author.append(author_part) # write to TCX file - tree.write( - TCX_FOLDER + "/" + fit_id + ".tcx", encoding="utf-8", xml_declaration=True - ) + xml_str = minidom.parseString(ET.tostring(training_center_database)).toprettyxml() + with open(TCX_FOLDER + "/" + fit_id + ".tcx", "w") as f: + f.write(str(xml_str)) # TODO time complexity is too heigh, need to be reduced @@ -346,9 +366,9 @@ def __call__(self, r): r.headers["timestamp"] = timestamp if "refresh_token" in params: r.headers["authorization"] = "Basic " + basic_auth - r.headers[ - "content-type" - ] = "application/x-www-form-urlencode; charset=utf-8" + r.headers["content-type"] = ( + "application/x-www-form-urlencode; charset=utf-8" + ) else: r.headers["authorization"] = "Bearer " + self.token r.headers["content-type"] = "application/json; charset=utf-8" diff --git a/run_page/config.py b/run_page/config.py index 1f1dd7e22b3..50b504148e6 100644 --- a/run_page/config.py +++ b/run_page/config.py @@ -26,7 +26,7 @@ BASE_TIMEZONE = "Asia/Shanghai" - +UTC_TIMEZONE = "UTC" start_point = namedtuple("start_point", "lat lon") run_map = namedtuple("polyline", "summary_polyline") diff --git a/run_page/coros_sync.py b/run_page/coros_sync.py new file mode 100644 index 00000000000..48193cd94cd --- /dev/null +++ b/run_page/coros_sync.py @@ -0,0 +1,166 @@ +import argparse +import asyncio +import hashlib +import os +import time + +import aiofiles +import httpx + +from config import JSON_FILE, SQL_FILE, FIT_FOLDER +from utils import make_activities_file + +COROS_URL_DICT = { + "LOGIN_URL": "https://teamcnapi.coros.com/account/login", + "DOWNLOAD_URL": "https://teamcnapi.coros.com/activity/detail/download", + "ACTIVITY_LIST": "https://teamcnapi.coros.com/activity/query?&modeList=100,102,103", +} + +TIME_OUT = httpx.Timeout(240.0, connect=360.0) + + +class Coros: + def __init__(self, account, password): + self.account = account + self.password = password + self.headers = None + self.req = None + + async def login(self): + url = COROS_URL_DICT.get("LOGIN_URL") + headers = { + "authority": "teamcnapi.coros.com", + "accept": "application/json, text/plain, */*", + "accept-language": "zh-CN,zh;q=0.9", + "content-type": "application/json;charset=UTF-8", + "dnt": "1", + "origin": "https://t.coros.com", + "referer": "https://t.coros.com/", + "sec-ch-ua": '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"macOS"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-site", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + } + data = {"account": self.account, "accountType": 2, "pwd": self.password} + async with httpx.AsyncClient(timeout=TIME_OUT) as client: + response = await client.post(url, json=data, headers=headers) + resp_json = response.json() + access_token = resp_json.get("data", {}).get("accessToken") + if not access_token: + raise Exception( + "============Login failed! please check your account and password===========" + ) + self.headers = { + "accesstoken": access_token, + "cookie": f"CPL-coros-region=2; CPL-coros-token={access_token}", + } + self.req = httpx.AsyncClient(timeout=TIME_OUT, headers=self.headers) + await client.aclose() + + async def init(self): + await self.login() + + async def fetch_activity_ids(self): + page_number = 1 + all_activities_ids = [] + + while True: + url = f"{COROS_URL_DICT.get('ACTIVITY_LIST')}&pageNumber={page_number}&size=20" + response = await self.req.get(url) + data = response.json() + activities = data.get("data", {}).get("dataList", None) + if not activities: + break + for activity in activities: + label_id = activity["labelId"] + if label_id is None: + continue + all_activities_ids.append(label_id) + + page_number += 1 + + return all_activities_ids + + async def download_activity(self, label_id): + download_folder = FIT_FOLDER + download_url = f"{COROS_URL_DICT.get('DOWNLOAD_URL')}?labelId={label_id}&sportType=100&fileType=4" + file_url = None + try: + response = await self.req.post(download_url) + resp_json = response.json() + file_url = resp_json.get("data", {}).get("fileUrl") + if not file_url: + print(f"No file URL found for label_id {label_id}") + return None, None + + fname = os.path.basename(file_url) + file_path = os.path.join(download_folder, fname) + + async with self.req.stream("GET", file_url) as response: + response.raise_for_status() + async with aiofiles.open(file_path, "wb") as f: + async for chunk in response.aiter_bytes(): + await f.write(chunk) + except httpx.HTTPStatusError as exc: + print( + f"Failed to download {file_url} with status code {response.status_code}: {exc}" + ) + return None, None + except Exception as exc: + print(f"Error occurred while downloading {file_url}: {exc}") + return None, None + + return label_id, fname + + +def get_downloaded_ids(folder): + return [i.split(".")[0] for i in os.listdir(folder) if not i.startswith(".")] + + +async def download_and_generate(account, password): + folder = FIT_FOLDER + downloaded_ids = get_downloaded_ids(folder) + coros = Coros(account, password) + await coros.init() + + activity_ids = await coros.fetch_activity_ids() + print("activity_ids: ", len(activity_ids)) + print("downloaded_ids: ", len(downloaded_ids)) + to_generate_coros_ids = list(set(activity_ids) - set(downloaded_ids)) + print("to_generate_activity_ids: ", len(to_generate_coros_ids)) + + start_time = time.time() + await gather_with_concurrency( + 10, + [coros.download_activity(label_d) for label_d in to_generate_coros_ids], + ) + print(f"Download finished. Elapsed {time.time()-start_time} seconds") + await coros.req.aclose() + make_activities_file(SQL_FILE, FIT_FOLDER, JSON_FILE, "fit") + + +async def gather_with_concurrency(n, tasks): + semaphore = asyncio.Semaphore(n) + + async def sem_task(task): + async with semaphore: + return await task + + return await asyncio.gather(*(sem_task(task) for task in tasks)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("account", nargs="?", help="input coros account") + + parser.add_argument("password", nargs="?", help="input coros password") + options = parser.parse_args() + + account = options.account + password = options.password + encrypted_pwd = hashlib.md5(password.encode()).hexdigest() + + asyncio.run(download_and_generate(account, encrypted_pwd)) diff --git a/run_page/endomondo_sync.py b/run_page/endomondo_sync.py index 1b330f5fa1f..bdecf438d68 100644 --- a/run_page/endomondo_sync.py +++ b/run_page/endomondo_sync.py @@ -2,6 +2,7 @@ need to download the files from endomondo and store it in Workous dir in running_page """ + import json import os from collections import namedtuple diff --git a/run_page/garmin_to_strava_sync.py b/run_page/garmin_to_strava_sync.py index c484ebee7c5..d7fe8a0cf70 100644 --- a/run_page/garmin_to_strava_sync.py +++ b/run_page/garmin_to_strava_sync.py @@ -2,6 +2,7 @@ new garmin ids to strava; not the same logic as nike_to_strava_sync """ + import argparse import asyncio import os diff --git a/run_page/generator/__init__.py b/run_page/generator/__init__.py index 9d91210de70..9c798718a6f 100644 --- a/run_page/generator/__init__.py +++ b/run_page/generator/__init__.py @@ -156,3 +156,16 @@ def get_old_tracks_ids(self): # pass the error print(f"something wrong with {str(e)}") return [] + + def get_old_tracks_dates(self): + try: + activities = ( + self.session.query(Activity) + .order_by(Activity.start_date_local.desc()) + .all() + ) + return [str(a.start_date_local) for a in activities] + except Exception as e: + # pass the error + print(f"something wrong with {str(e)}") + return [] diff --git a/run_page/generator/db.py b/run_page/generator/db.py index bd8ab04f46a..a153e38c343 100644 --- a/run_page/generator/db.py +++ b/run_page/generator/db.py @@ -83,13 +83,18 @@ def update_or_create_activity(session, run_activity): if not location_country and start_point or location_country == "China": try: location_country = str( - g.reverse(f"{start_point.lat}, {start_point.lon}") + g.reverse( + f"{start_point.lat}, {start_point.lon}", language="zh-CN" + ) ) # limit (only for the first time) except Exception as e: try: location_country = str( - g.reverse(f"{start_point.lat}, {start_point.lon}") + g.reverse( + f"{start_point.lat}, {start_point.lon}", + language="zh-CN", + ) ) except Exception as e: pass diff --git a/run_page/gpxtrackposter/circular_drawer.py b/run_page/gpxtrackposter/circular_drawer.py index 6deecf1ff38..cfd0a945223 100644 --- a/run_page/gpxtrackposter/circular_drawer.py +++ b/run_page/gpxtrackposter/circular_drawer.py @@ -1,4 +1,5 @@ """Draw a circular Poster.""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/gpxtrackposter/grid_drawer.py b/run_page/gpxtrackposter/grid_drawer.py index b53784aebd3..e4ae66f9eac 100644 --- a/run_page/gpxtrackposter/grid_drawer.py +++ b/run_page/gpxtrackposter/grid_drawer.py @@ -1,4 +1,5 @@ """Draw a grid poster.""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/gpxtrackposter/poster.py b/run_page/gpxtrackposter/poster.py index 9d42667a34c..6f9a45cd692 100644 --- a/run_page/gpxtrackposter/poster.py +++ b/run_page/gpxtrackposter/poster.py @@ -1,4 +1,5 @@ """Create a poster from track data.""" + import gettext import locale from collections import defaultdict diff --git a/run_page/gpxtrackposter/track.py b/run_page/gpxtrackposter/track.py index 3efbed2a447..75d219ac8a2 100644 --- a/run_page/gpxtrackposter/track.py +++ b/run_page/gpxtrackposter/track.py @@ -1,4 +1,5 @@ """Create and maintain info about a given activity track (corresponding to one GPX file).""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # 2019-now yihong0618 Florian Pigorsch & Contributors. All rights reserved. # Use of this source code is governed by a MIT-style @@ -247,9 +248,11 @@ def _load_fit_data(self, fit: dict): # moving_dict self.moving_dict["distance"] = message["total_distance"] self.moving_dict["moving_time"] = datetime.timedelta( - seconds=message["total_moving_time"] - if "total_moving_time" in message - else message["total_timer_time"] + seconds=( + message["total_moving_time"] + if "total_moving_time" in message + else message["total_timer_time"] + ) ) self.moving_dict["elapsed_time"] = datetime.timedelta( seconds=message["total_elapsed_time"] @@ -309,9 +312,11 @@ def _get_moving_data(gpx): "elapsed_time": datetime.timedelta( seconds=(moving_data.moving_time + moving_data.stopped_time) ), - "average_speed": moving_data.moving_distance / moving_data.moving_time - if moving_data.moving_time - else 0, + "average_speed": ( + moving_data.moving_distance / moving_data.moving_time + if moving_data.moving_time + else 0 + ), } def to_namedtuple(self): @@ -324,9 +329,9 @@ def to_namedtuple(self): "start_date_local": self.start_time_local.strftime("%Y-%m-%d %H:%M:%S"), "end_local": self.end_time_local.strftime("%Y-%m-%d %H:%M:%S"), "length": self.length, - "average_heartrate": int(self.average_heartrate) - if self.average_heartrate - else None, + "average_heartrate": ( + int(self.average_heartrate) if self.average_heartrate else None + ), "map": run_map(self.polyline_str), "start_latlng": self.start_latlng, } diff --git a/run_page/gpxtrackposter/track_loader.py b/run_page/gpxtrackposter/track_loader.py index b63cb9ba969..260b92af03b 100644 --- a/run_page/gpxtrackposter/track_loader.py +++ b/run_page/gpxtrackposter/track_loader.py @@ -1,6 +1,5 @@ """Handle parsing of GPX files""" - # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # 2019-now Yihong0618 # @@ -67,7 +66,7 @@ def __init__(self): "fit": load_fit_file, } - def load_tracks(self, data_dir, file_suffix): + def load_tracks(self, data_dir, file_suffix="gpx"): """Load tracks data_dir and return as a List of tracks""" file_names = [x for x in self._list_data_files(data_dir, file_suffix)] print(f"{file_suffix.upper()} files: {len(file_names)}") diff --git a/run_page/gpxtrackposter/tracks_drawer.py b/run_page/gpxtrackposter/tracks_drawer.py index fa42e0f332f..912de8ce952 100644 --- a/run_page/gpxtrackposter/tracks_drawer.py +++ b/run_page/gpxtrackposter/tracks_drawer.py @@ -1,4 +1,5 @@ """Contains the base class TracksDrawer, which other Drawers inherit from.""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/gpxtrackposter/utils.py b/run_page/gpxtrackposter/utils.py index 6b371ec1507..45acfadd85f 100644 --- a/run_page/gpxtrackposter/utils.py +++ b/run_page/gpxtrackposter/utils.py @@ -1,4 +1,5 @@ """Assorted utility methods for use in creating posters.""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/gpxtrackposter/value_range.py b/run_page/gpxtrackposter/value_range.py index 740c0dc03c5..907e55e0df1 100644 --- a/run_page/gpxtrackposter/value_range.py +++ b/run_page/gpxtrackposter/value_range.py @@ -1,4 +1,5 @@ """Represent a range of numerical values""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/gpxtrackposter/xy.py b/run_page/gpxtrackposter/xy.py index 507006c2bb6..c8ecd59e956 100644 --- a/run_page/gpxtrackposter/xy.py +++ b/run_page/gpxtrackposter/xy.py @@ -1,4 +1,5 @@ """Represent x,y coords with properly overloaded operations.""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/gpxtrackposter/year_range.py b/run_page/gpxtrackposter/year_range.py index d38da273cda..dacb977b9d9 100644 --- a/run_page/gpxtrackposter/year_range.py +++ b/run_page/gpxtrackposter/year_range.py @@ -1,4 +1,5 @@ """Represent a range of years, with ability to update based on a track""" + # Copyright 2016-2019 Florian Pigorsch & Contributors. All rights reserved. # # Use of this source code is governed by a MIT-style diff --git a/run_page/joyrun_sync.py b/run_page/joyrun_sync.py index 3f738cc7637..6441f198f62 100755 --- a/run_page/joyrun_sync.py +++ b/run_page/joyrun_sync.py @@ -130,7 +130,9 @@ def __update_loginInfo(self): self.session.headers.update({"ypcookie": loginCookie}) self.session.cookies.clear() self.session.cookies.set("ypcookie", quote(loginCookie).lower()) - self.session.headers.update(self.device_info_headers) # 更新设备信息中的 uid 字段 + self.session.headers.update( + self.device_info_headers + ) # 更新设备信息中的 uid 字段 def login_by_phone(self): params = { @@ -184,18 +186,38 @@ def parse_content_to_ponits(content): return points @staticmethod - def parse_points_to_gpx(run_points_data, start_time, end_time, interval=5): - # TODO for now kind of same as `keep` maybe refactor later + def parse_points_to_gpx( + run_points_data, start_time, end_time, pause_list, interval=5 + ): + """ + parse run_data content to gpx object + TODO for now kind of same as `keep` maybe refactor later + + :param run_points_data: [[latitude, longitude],...] + :param pause_list: [[interval_index, pause_seconds],...] + :param interval: time interval between each point, in seconds + """ + + # format data + segment_list = [] points_dict_list = [] - i = 0 - for point in run_points_data[:-1]: + current_time = start_time + + for index, point in enumerate(run_points_data[:-1]): points_dict = { "latitude": point[0], "longitude": point[1], - "time": datetime.utcfromtimestamp(start_time + interval * i), + "time": datetime.utcfromtimestamp(current_time), } - i += 1 points_dict_list.append(points_dict) + + current_time += interval + if pause_list and int(pause_list[0][0]) - 1 == index: + segment_list.append(points_dict_list[:]) + points_dict_list.clear() + current_time += int(pause_list[0][1]) + pause_list.pop(0) + points_dict_list.append( { "latitude": run_points_data[-1][0], @@ -203,18 +225,22 @@ def parse_points_to_gpx(run_points_data, start_time, end_time, interval=5): "time": datetime.utcfromtimestamp(end_time), } ) + segment_list.append(points_dict_list) + + # gpx part gpx = gpxpy.gpx.GPX() gpx.nsmap["gpxtpx"] = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1" gpx_track = gpxpy.gpx.GPXTrack() gpx_track.name = "gpx from joyrun" gpx.tracks.append(gpx_track) - # Create first segment in our GPX track: - gpx_segment = gpxpy.gpx.GPXTrackSegment() - gpx_track.segments.append(gpx_segment) - for p in points_dict_list: - point = gpxpy.gpx.GPXTrackPoint(**p) - gpx_segment.points.append(point) + # add segment list to our GPX track: + for point_list in segment_list: + gpx_segment = gpxpy.gpx.GPXTrackSegment() + gpx_track.segments.append(gpx_segment) + for p in point_list: + point = gpxpy.gpx.GPXTrackPoint(**p) + gpx_segment.points.append(point) return gpx.to_xml() @@ -237,12 +263,13 @@ def parse_raw_data_to_nametuple(self, run_data, old_gpx_ids, with_gpx=False): start_time = run_data["starttime"] end_time = run_data["endtime"] + pause_list = run_data["pause"] run_points_data = self.parse_content_to_ponits(run_data["content"]) if with_gpx: # pass the track no points if run_points_data: gpx_data = self.parse_points_to_gpx( - run_points_data, start_time, end_time + run_points_data, start_time, end_time, pause_list ) download_joyrun_gpx(gpx_data, str(joyrun_id)) try: diff --git a/run_page/keep_sync.py b/run_page/keep_sync.py index 80323c9b9e9..fe1c425eaad 100755 --- a/run_page/keep_sync.py +++ b/run_page/keep_sync.py @@ -17,10 +17,17 @@ from utils import adjust_time import xml.etree.ElementTree as ET +KEEP_SPORT_TYPES = ["running", "hiking", "cycling"] +KEEP2STRAVA = { + "outdoorWalking": "Walk", + "outdoorRunning": "Run", + "outdoorCycling": "Ride", + "indoorRunning": "VirtualRun", +} # need to test LOGIN_API = "https://api.gotokeep.com/v1.1/users/login" -RUN_DATA_API = "https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type=running&lastDate={last_date}" -RUN_LOG_API = "https://api.gotokeep.com/pd/v3/runninglog/{run_id}" +RUN_DATA_API = "https://api.gotokeep.com/pd/v3/stats/detail?dateUnit=all&type={sport_type}&lastDate={last_date}" +RUN_LOG_API = "https://api.gotokeep.com/pd/v3/{sport_type}log/{run_id}" HR_FRAME_THRESHOLD_IN_DECISECOND = 100 # Maximum time difference to consider a data point as the nearest, the unit is decisecond(分秒) @@ -43,11 +50,15 @@ def login(session, mobile, password): return session, headers -def get_to_download_runs_ids(session, headers): +def get_to_download_runs_ids(session, headers, sport_type): last_date = 0 result = [] + while 1: - r = session.get(RUN_DATA_API.format(last_date=last_date), headers=headers) + r = session.get( + RUN_DATA_API.format(sport_type=sport_type, last_date=last_date), + headers=headers, + ) if r.ok: run_logs = r.json()["data"]["records"] @@ -63,8 +74,10 @@ def get_to_download_runs_ids(session, headers): return result -def get_single_run_data(session, headers, run_id): - r = session.get(RUN_LOG_API.format(run_id=run_id), headers=headers) +def get_single_run_data(session, headers, run_id, sport_type): + r = session.get( + RUN_LOG_API.format(sport_type=sport_type, run_id=run_id), headers=headers + ) if r.ok: return r.json() @@ -82,7 +95,10 @@ def decode_runmap_data(text, is_geo=False): def parse_raw_data_to_nametuple( - run_data, old_gpx_ids, session, with_download_gpx=False + run_data, + old_gpx_ids, + session, + with_download_gpx=False, ): run_data = run_data["data"] run_points_data = [] @@ -119,11 +135,12 @@ def parse_raw_data_to_nametuple( if p_hr: p["hr"] = p_hr if with_download_gpx: - if ( - str(keep_id) not in old_gpx_ids - and run_data["dataType"] == "outdoorRunning" + if str(keep_id) not in old_gpx_ids and run_data["dataType"].startswith( + "outdoor" ): - gpx_data = parse_points_to_gpx(run_points_data_gpx, start_time) + gpx_data = parse_points_to_gpx( + run_points_data_gpx, start_time, KEEP2STRAVA[run_data["dataType"]] + ) download_keep_gpx(gpx_data, str(keep_id)) else: print(f"ID {keep_id} no gps data") @@ -139,9 +156,9 @@ def parse_raw_data_to_nametuple( return d = { "id": int(keep_id), - "name": "run from keep", + "name": f"{KEEP2STRAVA[run_data['dataType']]} from keep", # future to support others workout now only for run - "type": "Run", + "type": f"{KEEP2STRAVA[(run_data['dataType'])]}", "start_date": datetime.strftime(start_date, "%Y-%m-%d %H:%M:%S"), "end": datetime.strftime(end, "%Y-%m-%d %H:%M:%S"), "start_date_local": datetime.strftime(start_date_local, "%Y-%m-%d %H:%M:%S"), @@ -161,31 +178,34 @@ def parse_raw_data_to_nametuple( return namedtuple("x", d.keys())(*d.values()) -def get_all_keep_tracks(email, password, old_tracks_ids, with_download_gpx=False): +def get_all_keep_tracks( + email, password, old_tracks_ids, keep_sports_data_api, with_download_gpx=False +): if with_download_gpx and not os.path.exists(GPX_FOLDER): os.mkdir(GPX_FOLDER) s = requests.Session() s, headers = login(s, email, password) - runs = get_to_download_runs_ids(s, headers) - runs = [run for run in runs if run.split("_")[1] not in old_tracks_ids] - print(f"{len(runs)} new keep runs to generate") tracks = [] - old_gpx_ids = os.listdir(GPX_FOLDER) - old_gpx_ids = [i.split(".")[0] for i in old_gpx_ids if not i.startswith(".")] - for run in runs: - print(f"parsing keep id {run}") - try: - run_data = get_single_run_data(s, headers, run) - track = parse_raw_data_to_nametuple( - run_data, old_gpx_ids, s, with_download_gpx - ) - tracks.append(track) - except Exception as e: - print(f"Something wrong paring keep id {run}" + str(e)) + for api in keep_sports_data_api: + runs = get_to_download_runs_ids(s, headers, api) + runs = [run for run in runs if run.split("_")[1] not in old_tracks_ids] + print(f"{len(runs)} new keep {api} data to generate") + old_gpx_ids = os.listdir(GPX_FOLDER) + old_gpx_ids = [i.split(".")[0] for i in old_gpx_ids if not i.startswith(".")] + for run in runs: + print(f"parsing keep id {run}") + try: + run_data = get_single_run_data(s, headers, run, api) + track = parse_raw_data_to_nametuple( + run_data, old_gpx_ids, s, with_download_gpx + ) + tracks.append(track) + except Exception as e: + print(f"Something wrong paring keep id {run}" + str(e)) return tracks -def parse_points_to_gpx(run_points_data, start_time): +def parse_points_to_gpx(run_points_data, start_time, sport_type): """ Convert run points data to GPX format. @@ -219,6 +239,7 @@ def parse_points_to_gpx(run_points_data, start_time): gpx.nsmap["gpxtpx"] = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1" gpx_track = gpxpy.gpx.GPXTrack() gpx_track.name = "gpx from keep" + gpx_track.type = sport_type gpx.tracks.append(gpx_track) # Create first segment in our GPX track: @@ -292,15 +313,18 @@ def download_keep_gpx(gpx_data, keep_id): file_path = os.path.join(GPX_FOLDER, str(keep_id) + ".gpx") with open(file_path, "w") as fb: fb.write(gpx_data) + return file_path except: print(f"wrong id {keep_id}") pass -def run_keep_sync(email, password, with_download_gpx=False): +def run_keep_sync(email, password, keep_sports_data_api, with_download_gpx=False): generator = Generator(SQL_FILE) old_tracks_ids = generator.get_old_tracks_ids() - new_tracks = get_all_keep_tracks(email, password, old_tracks_ids, with_download_gpx) + new_tracks = get_all_keep_tracks( + email, password, old_tracks_ids, keep_sports_data_api, with_download_gpx + ) generator.sync_from_app(new_tracks) activities_list = generator.load() @@ -312,6 +336,13 @@ def run_keep_sync(email, password, with_download_gpx=False): parser = argparse.ArgumentParser() parser.add_argument("phone_number", help="keep login phone number") parser.add_argument("password", help="keep login password") + parser.add_argument( + "--sync-types", + dest="sync_types", + nargs="+", + default=["running"], + help="sync sport types from keep, default is running, you can choose from running, hiking, cycling", + ) parser.add_argument( "--with-gpx", dest="with_gpx", @@ -319,4 +350,10 @@ def run_keep_sync(email, password, with_download_gpx=False): help="get all keep data to gpx and download", ) options = parser.parse_args() - run_keep_sync(options.phone_number, options.password, options.with_gpx) + for _tpye in options.sync_types: + assert ( + _tpye in KEEP_SPORT_TYPES + ), f"{_tpye} are not supported type, please make sure that the type entered in the {KEEP_SPORT_TYPES}" + run_keep_sync( + options.phone_number, options.password, options.sync_types, options.with_gpx + ) diff --git a/run_page/keep_to_strava_sync.py b/run_page/keep_to_strava_sync.py new file mode 100644 index 00000000000..38647b1871b --- /dev/null +++ b/run_page/keep_to_strava_sync.py @@ -0,0 +1,146 @@ +import argparse +import json +import os +from sre_constants import SUCCESS +import time +from collections import namedtuple +import requests +from config import GPX_FOLDER +from Crypto.Cipher import AES +from config import OUTPUT_DIR +from stravalib.exc import ActivityUploadFailed, RateLimitTimeout +from utils import make_strava_client, upload_file_to_strava +from keep_sync import KEEP_DATA_TYPE_API, get_all_keep_tracks +from strava_sync import run_strava_sync + +""" +Only provide the ability to sync data from Keep's multiple sport types to Strava's corresponding sport types to help those who use multiple devices like me, the web page presentation still uses Strava (or refer to nike_to_strava_sync.py to modify it to suit you). +My own best practices: +1. running/hiking/Cycling (Huawei/OPPO) -> Keep +2. Keep -> Strava (add this scripts to run_data_sync.yml) +3. Road Cycling(Garmin) -> Strava. +4. running_page(Strava) + +""" +KEEP2STRAVA_BK_PATH = os.path.join(OUTPUT_DIR, "keep2strava.json") + + +def run_keep_sync(email, password, keep_sports_data_api, with_download_gpx=False): + + if not os.path.exists(KEEP2STRAVA_BK_PATH): + file = open(KEEP2STRAVA_BK_PATH, "w") + file.close() + content = [] + else: + with open(KEEP2STRAVA_BK_PATH) as f: + try: + content = json.loads(f.read()) + except: + content = [] + old_tracks_ids = [str(a["run_id"]) for a in content] + _new_tracks = get_all_keep_tracks( + email, password, old_tracks_ids, keep_sports_data_api, True + ) + new_tracks = [] + for track in _new_tracks: + # By default only outdoor sports have latlng as well as GPX. + if track.start_latlng is not None: + file_path = namedtuple("x", "gpx_file_path")( + os.path.join(GPX_FOLDER, str(track.id) + ".gpx") + ) + else: + file_path = namedtuple("x", "gpx_file_path")(None) + track = namedtuple("y", track._fields + file_path._fields)(*(track + file_path)) + new_tracks.append(track) + + return new_tracks + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("phone_number", help="keep login phone number") + parser.add_argument("password", help="keep login password") + parser.add_argument("client_id", help="strava client id") + parser.add_argument("client_secret", help="strava client secret") + parser.add_argument("strava_refresh_token", help="strava refresh token") + parser.add_argument( + "--sync-types", + dest="sync_types", + nargs="+", + default=["running"], + help="sync sport types from keep, default is running, you can choose from running, hiking, cycling", + ) + + options = parser.parse_args() + for api in options.sync_types: + assert ( + api in KEEP_DATA_TYPE_API + ), f"{api} are not supported type, please make sure that the type entered in the {KEEP_DATA_TYPE_API}" + new_tracks = run_keep_sync( + options.phone_number, options.password, options.sync_types, True + ) + + # to strava. + print("Need to load all gpx files maybe take some time") + last_time = 0 + client = make_strava_client( + options.client_id, options.client_secret, options.strava_refresh_token + ) + + index = 1 + print(f"Up to {len(new_tracks)} files are waiting to be uploaded") + uploaded_file_paths = [] + for track in new_tracks: + if track.gpx_file_path is not None: + try: + upload_file_to_strava(client, track.gpx_file_path, "gpx", False) + uploaded_file_paths.append(track) + except RateLimitTimeout as e: + timeout = e.timeout + print(f"Strava API Rate Limit Timeout. Retry in {timeout} seconds\n") + time.sleep(timeout) + # try previous again + upload_file_to_strava(client, track.gpx_file_path, "gpx", False) + uploaded_file_paths.append(track) + except ActivityUploadFailed as e: + print(f"Upload faild error {str(e)}") + # spider rule + time.sleep(1) + else: + # for no gps data, like indoorRunning. + uploaded_file_paths.append(track) + time.sleep(10) + + # This file is used to record which logs have been uploaded to strava + # to avoid intrusion into the data.db resulting in double counting of data. + with open(KEEP2STRAVA_BK_PATH, "r") as f: + try: + content = json.loads(f.read()) + except: + content = [] + + # Extend and Save the successfully uploaded log to the backup file. + content.extend( + [ + dict( + run_id=track.id, + name=track.name, + type=track.type, + gpx_file_path=track.gpx_file_path, + ) + for track in uploaded_file_paths + ] + ) + with open(KEEP2STRAVA_BK_PATH, "w") as f: + json.dump(content, f, indent=0) + + # del the uploaded GPX file. + for track in uploaded_file_paths: + if track.gpx_file_path is not None: + os.remove(track.gpx_file_path) + else: + continue + + run_strava_sync( + options.client_id, options.client_secret, options.strava_refresh_token + ) diff --git a/run_page/oppo_sync.py b/run_page/oppo_sync.py new file mode 100644 index 00000000000..cf5bf803f6b --- /dev/null +++ b/run_page/oppo_sync.py @@ -0,0 +1,727 @@ +import argparse +import hashlib +import json +import os +import time +import xml.etree.ElementTree as ET +from collections import namedtuple +from datetime import datetime, timedelta +from xml.dom import minidom + +import gpxpy +import polyline +import requests +from tzlocal import get_localzone + +from config import ( + GPX_FOLDER, + JSON_FILE, + SQL_FILE, + run_map, + start_point, + TCX_FOLDER, + UTC_TIMEZONE, +) +from generator import Generator +from utils import adjust_time + +TOKEN_REFRESH_URL = "https://sport.health.heytapmobi.com/open/v1/oauth/token" +OPPO_HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", + "Content-Type": "application/json", + "Accept": "application/json", +} + +# Query brief version of sports records +# The query range cannot exceed one month! +""" +Return value is like: +[ + { + "dataType": 2,//运动数类型 1=健身类 2=其他运动类 + "startTime": 1630323565000, //开始时间 单位毫秒 + "endTime": 1630337130000,//结束时间 单位毫秒 + "sportMode": 10,//运动模式 室内跑 详情见文档附录 + "otherSportData": { + "avgHeartRate": 153,//平均心率 单位:count/min + "avgPace": 585,//平均配速 单位s/km + "avgStepRate": 115,//平均步频 单位step/min + "bestStepRate": 135,//最佳步频 单位step/min + "bestPace": 572,//最佳配速 单位s/km + "totalCalories": 2176000,//总消耗 单位卡 + "totalDistance": 23175,//总距离 单位米 + "totalSteps": 26062,//总步数 + "totalTime": 13562000,//总时长,单位:毫秒 + "totalClimb": 100//累计爬升高度,单位:米 + }, + }, + { + "dataType": 1,//运动数类型 1=健身类 2=其他运动类 + "startTime": 1630293981497 //开始时间 单位毫秒 + "endTime": 1630294218127,//结束时间 单位毫秒 + "sportMode": 9,//运动模式 健身 详情见文档附录 + "fitnessData": { + "avgHeartRate": 90,//平均心率 单位:count/min + "courseName": "零基础减脂碎片练习",//课程名称 + "finishNumber": 1,//课程完成次数 + "trainedCalorie": 13554,//训练消耗的卡路里,单位:卡 + "trainedDuration": 176000//实际训练时间,单位:ms + }, + } +] +""" +BRIEF_SPORT_DATA_API = "https://sport.health.heytapmobi.com/open/v1/data/sport/record?startTimeMillis={start_time}&endTimeMillis={end_time}" + +# Query detailed sports records +# The query range cannot exceed one day! +DETAILED_SPORT_DATA_API = "https://sport.health.heytapmobi.com/open/v2/data/sport/record?startTimeMillis={start_time}&endTimeMillis={end_time}" + +TIMESTAMP_THRESHOLD_IN_MILLISECOND = 5000 + +# If your points need trans from gcj02 to wgs84 coordinate which use by Mapbox +TRANS_GCJ02_TO_WGS84 = True + +# May be Forerunner 945? +CONNECT_API_PART_NUMBER = "006-D2449-00" + +AVAILABLE_OUTDOOR_SPORT_MODE = [ + 1, # WALK + 2, # RUN + 3, # RIDE + 13, # OUTDOOR_PHYSICAL_RUN + 15, # OUTDOOR_5KM_RELAX_RUN + 17, # OUTDOOR_FAT_REDUCE_RUN + 22, # MARATHON + 36, # MOUNTAIN_CLIMBING + 37, # CROSS_COUNTRY +] + +AVAILABLE_INDOOR_SPORT_MODE = [ + 10, # INDOOR_RUN + 14, # INDOOR_PHYSICAL_RUN + 16, # INDOOR_5KM_RELAX_RUN + 18, # INDOOR_FAT_REDUCE_RUN + 19, # INDOOR_FITNESS_WALK + 21, # TREADMILL_RUN +] + + +def get_access_token(session, client_id, client_secret, refresh_token): + headers = { + "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0", + "Content-Type": "application/json", + "Accept": "application/json", + } + data = { + "clientId": client_id, + "clientSecret": client_secret, + "refreshToken": refresh_token, + "grantType": "refreshToken", + } + r = session.post(TOKEN_REFRESH_URL, headers=headers, json=data) + if r.ok: + token = r.json()["body"]["accessToken"] + headers["access-token"] = token + return session, headers + + +def get_to_download_runs_ranges(session, sync_months, headers, start_timestamp): + result = [] + current_time = datetime.now() + start_datatime = datetime.fromtimestamp(start_timestamp / 1000) + + if start_datatime < current_time + timedelta(days=-30 * sync_months): + """retrieve the data of last 6 months.""" + while sync_months >= 0: + temp_end = int(current_time.timestamp() * 1000) + current_time = current_time + timedelta(days=-30) + temp_start = int(current_time.timestamp() * 1000) + sync_months = sync_months - 1 + result.extend( + parse_brief_sport_data(session, headers, temp_start, temp_end) + ) + else: + while start_datatime < current_time: + temp_start = int(start_datatime.timestamp() * 1000) + start_datatime = start_datatime + timedelta(days=30) + temp_end = int(start_datatime.timestamp() * 1000) + result.extend( + parse_brief_sport_data(session, headers, temp_start, temp_end) + ) + return result + + +def parse_brief_sport_data(session, headers, temp_start, temp_end): + result = [] + r = session.get( + BRIEF_SPORT_DATA_API.format(end_time=temp_end, start_time=temp_start), + headers=headers, + ) + if r.ok: + sport_logs = r.json()["body"] + for i in sport_logs: + if ( + i["sportMode"] in AVAILABLE_INDOOR_SPORT_MODE + or i["sportMode"] in AVAILABLE_OUTDOOR_SPORT_MODE + ): + result.append((i["startTime"], i["endTime"])) + print(f"sync record: start_time: " + str(i["startTime"])) + time.sleep(1) # spider rule + return result + + +def get_single_run_data(session, headers, start, end): + r = session.get( + DETAILED_SPORT_DATA_API.format(end_time=end, start_time=start), headers=headers + ) + if r.ok: + return r.json() + + +def parse_raw_data_to_name_tuple(sport_data, with_gpx, with_tcx): + sport_data = sport_data["body"][0] + m = hashlib.md5() + m.update(str.encode(str(sport_data))) + oppo_id_str = str(int(m.hexdigest(), 16))[0:16] + oppo_id = int(oppo_id_str) + + sport_data["id"] = oppo_id + start_time = sport_data["startTime"] + other_data = sport_data["otherSportData"] + avg_heart_rate = None + if other_data: + avg_heart_rate = other_data.get("avgHeartRate", None) + # fix #66 + if avg_heart_rate and avg_heart_rate < 0: + avg_heart_rate = None + + # if TRANS_GCJ02_TO_WGS84: + # run_points_data = [ + # list(eviltransform.gcj2wgs(p["latitude"], p["longitude"])) + # for p in run_points_data + # ] + # for i, p in enumerate(run_points_data_gpx): + # p["latitude"] = run_points_data[i][0] + # p["longitude"] = run_points_data[i][1] + + point_dict = prepare_track_points(sport_data, with_gpx) + + if with_gpx is True: + gpx_data = parse_points_to_gpx(sport_data, point_dict) + download_keep_gpx(gpx_data, str(oppo_id)) + if with_tcx is True: + parse_points_to_tcx(sport_data, point_dict) + + else: + print(f"ID {oppo_id} no gps data") + + gps_data = [ + (item["latitude"], item["longitude"]) for item in other_data["gpsPoint"] + ] + polyline_str = polyline.encode(gps_data) if gps_data else "" + start_latlng = start_point(*gps_data[0]) if gps_data else None + start_date = datetime.utcfromtimestamp(start_time / 1000) + start_date_local = adjust_time(start_date, str(get_localzone())) + end = datetime.utcfromtimestamp(sport_data["endTime"] / 1000) + end_local = adjust_time(end, str(get_localzone())) + location_country = None + if not other_data["totalTime"]: + print(f"ID {oppo_id} has no total time just ignore please check") + return + d = { + "id": int(oppo_id), + "name": "activity from oppo", + # future to support others workout now only for run + "type": map_oppo_fit_type_to_strava_activity_type(sport_data["sportMode"]), + "start_date": datetime.strftime(start_date, "%Y-%m-%d %H:%M:%S"), + "end": datetime.strftime(end, "%Y-%m-%d %H:%M:%S"), + "start_date_local": datetime.strftime(start_date_local, "%Y-%m-%d %H:%M:%S"), + "end_local": datetime.strftime(end_local, "%Y-%m-%d %H:%M:%S"), + "length": other_data["totalDistance"], + "average_heartrate": int(avg_heart_rate) if avg_heart_rate else None, + "map": run_map(polyline_str), + "start_latlng": start_latlng, + "distance": other_data["totalDistance"], + "moving_time": timedelta(seconds=other_data["totalTime"]), + "elapsed_time": timedelta( + seconds=int((sport_data["endTime"] - sport_data["startTime"]) / 1000) + ), + "average_speed": other_data["totalDistance"] / other_data["totalTime"] * 1000, + "location_country": location_country, + "source": sport_data["deviceName"], + } + return namedtuple("x", d.keys())(*d.values()) + + +def get_all_oppo_tracks( + client_id, + client_secret, + refresh_token, + sync_months, + last_track_date, + with_download_gpx, + with_download_tcx, +): + if with_download_gpx and not os.path.exists(GPX_FOLDER): + os.mkdir(GPX_FOLDER) + s = requests.Session() + s, headers = get_access_token(s, client_id, client_secret, refresh_token) + + last_timestamp = ( + 0 + if (last_track_date == 0) + else int( + datetime.timestamp(datetime.strptime(last_track_date, "%Y-%m-%d %H:%M:%S")) + * 1000 + ) + ) + + runs = get_to_download_runs_ranges(s, sync_months, headers, last_timestamp + 1000) + print(f"{len(runs)} new oppo runs to generate") + tracks = [] + for start, end in runs: + print(f"parsing oppo id {str(start)}-{str(end)}") + try: + run_data = get_single_run_data(s, headers, start, end) + track = parse_raw_data_to_name_tuple( + run_data, with_download_gpx, with_download_tcx + ) + tracks.append(track) + except Exception as e: + print(f"Something wrong paring keep id {str(start)}-{str(end)}" + str(e)) + return tracks + + +def switch(v): + yield lambda *c: v in c + + +def map_oppo_fit_type_to_gpx_type(oppo_type): + for case in switch(oppo_type): + if case(1): # WALK + return "Walking" + if case(2, 13, 15, 17, 22, 10, 14, 16, 18, 21, 37): + # RUN | + # OUTDOOR_PHYSICAL_RUN | + # OUTDOOR_5KM_RELAX_RUN | + # OUTDOOR_FAT_REDUCE_RUN | + # MARATHON + # INDOOR_RUN, etc. + # CROSS_COUNTRY + return "Running" + if case(19): # MOUNTAIN_CLIMBING + return "Hiking" + if case(3): # Ride + return "Biking" + + +def map_oppo_fit_type_to_strava_activity_type(oppo_type): + """ + Note: should consider the supported strava activity type: + Link: https://developers.strava.com/docs/reference/#api-models-ActivityType + """ + for case in switch(oppo_type): + if case(1): # WALK + return "Walk" + if case(2, 13, 15, 17, 22, 10, 14, 16, 18, 21, 37): + # RUN | + # OUTDOOR_PHYSICAL_RUN | + # OUTDOOR_5KM_RELAX_RUN | + # OUTDOOR_FAT_REDUCE_RUN | + # MARATHON + # INDOOR_RUN, etc. + # CROSS_COUNTRY + return "Run" + if case(19): # MOUNTAIN_CLIMBING + return "Hike" + if case(3): # Ride + return "Ride" + + +def parse_points_to_gpx(sport_data, points_dict_list): + gpx = gpxpy.gpx.GPX() + gpx.nsmap["gpxtpx"] = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1" + gpx_track = gpxpy.gpx.GPXTrack() + gpx_track.name = f"""gpx from {sport_data["deviceName"]}""" + gpx_track.type = map_oppo_fit_type_to_gpx_type(sport_data["sportMode"]) + gpx.tracks.append(gpx_track) + + # Create first segment in our GPX track: + gpx_segment = gpxpy.gpx.GPXTrackSegment() + gpx_track.segments.append(gpx_segment) + for p in points_dict_list: + point = gpxpy.gpx.GPXTrackPoint( + latitude=p["latitude"], + longitude=p["longitude"], + time=p["time"], + elevation=p.get("elevation"), + ) + hr = p.get("hr") + cad = p.get("cad") + if hr is not None or cad is not None: + hr_str = f"""{hr}""" if hr is not None else "" + cad_str = ( + f"""{p["cad"]}""" if cad is not None else "" + ) + gpx_extension = ET.fromstring( + f""" + {hr_str} + {cad_str} + + """ + ) + point.extensions.append(gpx_extension) + gpx_segment.points.append(point) + return gpx.to_xml() + + +def download_keep_gpx(gpx_data, keep_id): + try: + print(f"downloading keep_id {str(keep_id)} gpx") + file_path = os.path.join(GPX_FOLDER, str(keep_id) + ".gpx") + with open(file_path, "w") as fb: + fb.write(gpx_data) + except: + print(f"wrong id {keep_id}") + pass + + +def prepare_track_points(sport_data, with_gpx): + """ + Convert run points data to GPX format. + + Args: + sport_data (map of dict): A map of run data points. + with_gpx (boolean): export to gpx file or not. + + Returns: + points_dict_list (list): data with need to parse. + """ + other_data = sport_data["otherSportData"] + decoded_hr_data = other_data.get("heartRate", None) + points_dict_list = [] + + if other_data.get("gpsPoint"): + timestamp_list = [item["timestamp"] for item in decoded_hr_data] + other_data = sport_data["otherSportData"] + value_size = len(other_data.get("gpsPoint", None)) + + for i in range(value_size): + temp_timestamp = other_data.get("gpsPoint")[i]["timestamp"] + j = timestamp_list.index(temp_timestamp) + + points_dict = { + "latitude": other_data.get("gpsPoint")[i]["latitude"], + "longitude": other_data.get("gpsPoint")[i]["longitude"], + "time": datetime.utcfromtimestamp(temp_timestamp / 1000), + "hr": other_data.get("heartRate")[j]["value"], + } + points_dict_list.append(get_value(j, points_dict, other_data)) + elif with_gpx is False: + value_size = len(other_data.get("heartRate", None)) + + for i in range(value_size): + temp_timestamp = other_data.get("heartRate")[i]["timestamp"] + temp_date = datetime.utcfromtimestamp(temp_timestamp / 1000) + points_dict = { + "time": temp_date, + "hr": other_data.get("heartRate")[i]["value"], + } + points_dict_list.append(get_value(i, points_dict, other_data)) + + return points_dict_list + + +def get_value(index, points_dict, other_data): + if other_data.get("pace"): + pace = other_data.get("pace")[index]["value"] + points_dict["speed"] = 0 if pace == 0 else 1000 / pace + if other_data.get("frequency"): + points_dict["cad"] = other_data.get("frequency")[index]["value"] + if other_data.get("distance"): + points_dict["distance"] = other_data.get("distance")[index]["value"] + if other_data.get("elevation"): + points_dict["elevation"] = other_data.get("elevation")[index]["value"] + return points_dict + + +def parse_points_to_tcx(sport_data, points_dict_list): + # route ID + fit_id = str(sport_data["id"]) + # local time + start_time = sport_data["startTime"] + start_date = datetime.utcfromtimestamp(start_time / 1000) + fit_start_time = datetime.strftime( + adjust_time(start_date, UTC_TIMEZONE), "%Y-%m-%dT%H:%M:%SZ" + ) + + # Root node + training_center_database = ET.Element( + "TrainingCenterDatabase", + { + "xmlns": "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2", + "xmlns:ns5": "http://www.garmin.com/xmlschemas/ActivityGoals/v1", + "xmlns:ns3": "http://www.garmin.com/xmlschemas/ActivityExtension/v2", + "xmlns:ns2": "http://www.garmin.com/xmlschemas/UserProfile/v2", + "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", + "xmlns:ns4": "http://www.garmin.com/xmlschemas/ProfileExtension/v1", + "xsi:schemaLocation": "http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd", + }, + ) + # xml tree + ET.ElementTree(training_center_database) + # Activities + activities = ET.Element("Activities") + training_center_database.append(activities) + # sport type + sports_type = map_oppo_fit_type_to_gpx_type(sport_data["sportMode"]) + # activity + activity = ET.Element("Activity", {"Sport": sports_type}) + activities.append(activity) + # Id + activity_id = ET.Element("Id") + activity_id.text = fit_start_time # Codoon use start_time as ID + activity.append(activity_id) + # Creator + activity_creator = ET.Element("Creator", {"xsi:type": "Device_t"}) + activity.append(activity_creator) + # Name + activity_creator_name = ET.Element("Name") + activity_creator_name.text = sport_data["deviceName"] + activity_creator.append(activity_creator_name) + activity_creator_product = ET.Element("ProductID") + activity_creator_product.text = "3441" + activity_creator.append(activity_creator_product) + + """ + first, find distance split index + """ + lap_split_indexes = [0] + points_dict_list_chunks = [] + + for idx, item in enumerate(points_dict_list): + size = len(lap_split_indexes) + if sports_type == "Running": + target_distance = 1000 * size + elif sports_type == "Biking": + target_distance = 5000 * size + else: + break + + if idx + 1 != len(points_dict_list): + if ( + item["distance"] + < target_distance + <= points_dict_list[idx + 1]["distance"] + ): + lap_split_indexes.append(idx) + + if len(lap_split_indexes) == 1: + points_dict_list_chunks = [points_dict_list] + else: + for idx, item in enumerate(lap_split_indexes): + if idx + 1 == len(lap_split_indexes): + points_dict_list_chunks.append( + points_dict_list[item : len(points_dict_list) - 1] + ) + else: + points_dict_list_chunks.append( + points_dict_list[item : lap_split_indexes[idx + 1]] + ) + + current_distance = 0 + current_time = start_date + + for item in points_dict_list_chunks: + # Lap + lap_start_time = datetime.strftime( + adjust_time(item[0]["time"], UTC_TIMEZONE), "%Y-%m-%dT%H:%M:%SZ" + ) + activity_lap = ET.Element("Lap", {"StartTime": lap_start_time}) + activity.append(activity_lap) + + # DistanceMeters + total_distance_node = ET.Element("DistanceMeters") + total_distance_node.text = str(item[-1]["distance"] - current_distance) + current_distance = item[-1]["distance"] + activity_lap.append(total_distance_node) + # TotalTimeSeconds + chile_node = ET.Element("TotalTimeSeconds") + chile_node.text = str((item[-1]["time"] - current_time).total_seconds()) + current_time = item[-1]["time"] + activity_lap.append(chile_node) + # MaximumSpeed + chile_node = ET.Element("MaximumSpeed") + chile_node.text = str(max(node["speed"] for node in item)) + activity_lap.append(chile_node) + # # Calories + # chile_node = ET.Element("Calories") + # chile_node.text = str(int(other_data["totalCalories"] / 1000)) + # activity_lap.append(chile_node) + # AverageHeartRateBpm + # bpm = ET.Element("AverageHeartRateBpm") + # bpm_value = ET.Element("Value") + # bpm.append(bpm_value) + # bpm_value.text = str(other_data["avgHeartRate"]) + # heartrate_list = [item["value"] for item in other_data["heartRate"]] + # bpm_value.text = str(round(statistics.mean(heartrate_list))) + # activity_lap.append(bpm) + # # MaximumHeartRateBpm + # bpm = ET.Element("MaximumHeartRateBpm") + # bpm_value = ET.Element("Value") + # bpm.append(bpm_value) + # bpm_value.text = str(max(node["hr"] for node in item)) + # activity_lap.append(bpm) + + # Track + track = ET.Element("Track") + activity_lap.append(track) + + for p in item: + tp = ET.Element("Trackpoint") + track.append(tp) + # Time + time_stamp = datetime.strftime( + adjust_time(p["time"], UTC_TIMEZONE), "%Y-%m-%dT%H:%M:%SZ" + ) + time_label = ET.Element("Time") + time_label.text = time_stamp + + tp.append(time_label) + if sports_type == "Biking" and p.get("cad"): + cadence_label = ET.Element("Cadence") + cadence_label.text = str(p["cad"]) + tp.append(cadence_label) + if p.get("distance"): + distance_label = ET.Element("DistanceMeters") + distance_label.text = str(p["distance"]) + tp.append(distance_label) + # HeartRateBpm + # None was converted to bytes by np.dtype, becoming a string "None" after decode...-_- + # as well as LatitudeDegrees and LongitudeDegrees below + if p.get("hr"): + bpm = ET.Element("HeartRateBpm") + bpm_value = ET.Element("Value") + bpm.append(bpm_value) + bpm_value.text = str(p["hr"]) + tp.append(bpm) + # AltitudeMeters + if p.get("elevation"): + altitude_meters = ET.Element("AltitudeMeters") + altitude_meters.text = str(p["elevation"] / 10) + tp.append(altitude_meters) + if p.get("latitude"): + position = ET.Element("Position") + tp.append(position) + # LatitudeDegrees + lati = ET.Element("LatitudeDegrees") + lati.text = str(p["latitude"]) + position.append(lati) + # LongitudeDegrees + longi = ET.Element("LongitudeDegrees") + longi.text = str(p["longitude"]) + position.append(longi) + # Extensions + if p.get("speed") is not None or ( + p.get("cad") is not None and sports_type == "Running" + ): + extensions = ET.Element("Extensions") + tp.append(extensions) + tpx = ET.Element("ns3:TPX") + extensions.append(tpx) + # LatitudeDegrees + # LatitudeDegrees + if p.get("speed") is not None: + speed = ET.Element("ns3:Speed") + speed.text = str(p["speed"]) + tpx.append(speed) + if p.get("cad") is not None and sports_type == "Running": + cad = ET.Element("ns3:RunCadence") + cad.text = str(round(p["cad"] / 2)) + tpx.append(cad) + # Author + author = ET.Element("Author", {"xsi:type": "Application_t"}) + training_center_database.append(author) + author_name = ET.Element("Name") + author_name.text = "Connect Api" + author.append(author_name) + author_lang = ET.Element("LangID") + author_lang.text = "en" + author.append(author_lang) + author_part = ET.Element("PartNumber") + author_part.text = CONNECT_API_PART_NUMBER + author.append(author_part) + # write to TCX file + xml_str = minidom.parseString(ET.tostring(training_center_database)).toprettyxml() + with open(TCX_FOLDER + "/" + fit_id + ".tcx", "w") as f: + f.write(str(xml_str)) + + +def formated_input( + run_data, run_data_label, tcx_label +): # load run_data from run_data_label, parse to tcx_label, return xml node + fit_data = str(run_data[run_data_label]) + chile_node = ET.Element(tcx_label) + chile_node.text = fit_data + return chile_node + + +def run_oppo_sync( + client_id, + client_secret, + refresh_token, + sync_months=6, + with_download_gpx=False, + with_download_tcx=True, +): + generator = Generator(SQL_FILE) + old_tracks_dates = generator.get_old_tracks_dates() + new_tracks = get_all_oppo_tracks( + client_id, + client_secret, + refresh_token, + sync_months, + old_tracks_dates[0] if old_tracks_dates else 0, + with_download_gpx, + with_download_tcx, + ) + generator.sync_from_app(new_tracks) + + activities_list = generator.load() + with open(JSON_FILE, "w") as f: + json.dump(activities_list, f, indent=0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("client_id", help="oppo heytap fit client id") + parser.add_argument("client_secret", help="oppo heytap fit client secret") + parser.add_argument("refresh_token", help="oppo heytap fit refresh token") + parser.add_argument( + "--with-gpx", + dest="with_gpx", + action="store_true", + help="get all oppo fit data to gpx and download", + ) + parser.add_argument( + "--with-tcx", + dest="with_tcx", + action="store_true", + help="get all oppo fit data to tcx and download", + ) + parser.add_argument( + "-m" "--months", + type=int, + default=6, + dest="sync_months", + help="oppo has limited the data retrieve, so the default months we can sync is 6.", + ) + options = parser.parse_args() + run_oppo_sync( + options.client_id, + options.client_secret, + options.refresh_token, + options.sync_months, + options.with_gpx, + options.with_tcx, + ) diff --git a/run_page/tcx_to_garmin_sync.py b/run_page/tcx_to_garmin_sync.py new file mode 100644 index 00000000000..6a3a42ba64a --- /dev/null +++ b/run_page/tcx_to_garmin_sync.py @@ -0,0 +1,81 @@ +import argparse +import asyncio +import os +from datetime import datetime + +from tcxreader.tcxreader import TCXReader + +from config import TCX_FOLDER +from garmin_sync import Garmin + + +def get_to_generate_files(last_time): + """ + return to one sorted list for next time upload + """ + file_names = os.listdir(TCX_FOLDER) + tcx = TCXReader() + tcx_files = [ + ( + tcx.read(os.path.join(TCX_FOLDER, i), only_gps=False), + os.path.join(TCX_FOLDER, i), + ) + for i in file_names + if i.endswith(".tcx") + ] + tcx_files_dict = { + int(i[0].trackpoints[0].time.timestamp()): i[1] + for i in tcx_files + if len(i[0].trackpoints) > 0 + and int(i[0].trackpoints[0].time.timestamp()) > last_time + } + + dict(sorted(tcx_files_dict.items())) + + return tcx_files_dict.values() + + +async def upload_tcx_files_to_garmin(options): + print("Need to load all tcx files maybe take some time") + garmin_auth_domain = "CN" if options.is_cn else "" + garmin_client = Garmin(options.secret_string, garmin_auth_domain) + + last_time = 0 + if not options.all: + print("upload new tcx to Garmin") + last_activity = await garmin_client.get_activities(0, 1) + if not last_activity: + print("no garmin activity") + else: + after_datetime_str = last_activity[0]["startTimeGMT"] + after_datetime = datetime.strptime(after_datetime_str, "%Y-%m-%d %H:%M:%S") + last_time = datetime.timestamp(after_datetime) + else: + print("Need to load all tcx files maybe take some time") + to_upload_dict = get_to_generate_files(last_time) + + await garmin_client.upload_activities_files(to_upload_dict) + + +if __name__ == "__main__": + if not os.path.exists(TCX_FOLDER): + os.mkdir(TCX_FOLDER) + parser = argparse.ArgumentParser() + parser.add_argument( + "secret_string", nargs="?", help="secret_string fro get_garmin_secret.py" + ) + parser.add_argument( + "--all", + dest="all", + action="store_true", + help="if upload to strava all without check last time", + ) + parser.add_argument( + "--is-cn", + dest="is_cn", + action="store_true", + help="if garmin account is cn", + ) + loop = asyncio.get_event_loop() + future = asyncio.ensure_future(upload_tcx_files_to_garmin(parser.parse_args())) + loop.run_until_complete(future) diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index a98cda8c369..1807e625b82 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -6,23 +6,20 @@ const Header = () => { return ( <> -