From 23464b5224c4b130d5e8f2dd73d7d805e730e019 Mon Sep 17 00:00:00 2001 From: Ben Bell Date: Sat, 28 Oct 2023 21:20:50 +0800 Subject: [PATCH 01/11] feat: add NavigationControl (#534) --- src/components/RunMap/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/RunMap/index.tsx b/src/components/RunMap/index.tsx index 819201c1a21..8164d1c0d77 100644 --- a/src/components/RunMap/index.tsx +++ b/src/components/RunMap/index.tsx @@ -1,6 +1,6 @@ import MapboxLanguage from '@mapbox/mapbox-gl-language'; import React, { useRef, useCallback } from 'react'; -import Map, { Layer, Source, FullscreenControl, MapRef } from 'react-map-gl'; +import Map, { Layer, Source, FullscreenControl, NavigationControl, MapRef } from 'react-map-gl'; import useActivities from '@/hooks/useActivities'; import { MAP_LAYER_LIST, @@ -141,6 +141,7 @@ const RunMap = ({ )} {title} + ); }; From 272c08178fc2831729e3f6eb22366f3e2d331576 Mon Sep 17 00:00:00 2001 From: HankZhao Date: Sun, 29 Oct 2023 03:23:43 -0500 Subject: [PATCH 02/11] feat:suport heart_rate in export gpx file (#529) * feat:suport heart_rate in export gpx file --- run_page/keep_sync.py | 119 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 17 deletions(-) diff --git a/run_page/keep_sync.py b/run_page/keep_sync.py index 332f313f03c..80323c9b9e9 100755 --- a/run_page/keep_sync.py +++ b/run_page/keep_sync.py @@ -1,7 +1,6 @@ import argparse import base64 import json -import math import os import time import zlib @@ -16,12 +15,16 @@ from Crypto.Cipher import AES from generator import Generator from utils import adjust_time +import xml.etree.ElementTree as ET # 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}" +HR_FRAME_THRESHOLD_IN_DECISECOND = 100 # Maximum time difference to consider a data point as the nearest, the unit is decisecond(分秒) + +TIMESTAMP_THRESHOLD_IN_DECISECOND = 3_600_000 # Threshold for target timestamp adjustment, the unit of timestamp is decisecond(分秒), so the 3_600_000 stands for 100 hours sports time. 100h = 100 * 60 * 60 * 10 # If your points need trans from gcj02 to wgs84 coordinate which use by Mapbox TRANS_GCJ02_TO_WGS84 = True @@ -88,6 +91,17 @@ def parse_raw_data_to_nametuple( keep_id = run_data["id"].split("_")[1] start_time = run_data["startTime"] + avg_heart_rate = None + decoded_hr_data = [] + if run_data["heartRate"]: + avg_heart_rate = run_data["heartRate"].get("averageHeartRate", None) + heart_rate_data = run_data["heartRate"].get("heartRates", None) + if heart_rate_data is not None: + decoded_hr_data = decode_runmap_data(heart_rate_data) + # fix #66 + if avg_heart_rate and avg_heart_rate < 0: + avg_heart_rate = None + if run_data["geoPoints"]: run_points_data = decode_runmap_data(run_data["geoPoints"], True) run_points_data_gpx = run_points_data @@ -99,20 +113,20 @@ def parse_raw_data_to_nametuple( for i, p in enumerate(run_points_data_gpx): p["latitude"] = run_points_data[i][0] p["longitude"] = run_points_data[i][1] - else: - run_points_data = [[p["latitude"], p["longitude"]] for p in run_points_data] + + for p in run_points_data_gpx: + p_hr = find_nearest_hr(decoded_hr_data, int(p["timestamp"]), start_time) + if p_hr: + p["hr"] = p_hr if with_download_gpx: - if str(keep_id) not in old_gpx_ids: + if ( + str(keep_id) not in old_gpx_ids + and run_data["dataType"] == "outdoorRunning" + ): gpx_data = parse_points_to_gpx(run_points_data_gpx, start_time) download_keep_gpx(gpx_data, str(keep_id)) else: print(f"ID {keep_id} no gps data") - heart_rate = None - if run_data["heartRate"]: - heart_rate = run_data["heartRate"].get("averageHeartRate", None) - # fix #66 - if heart_rate and heart_rate < 0: - heart_rate = None polyline_str = polyline.encode(run_points_data) if run_points_data else "" start_latlng = start_point(*run_points_data[0]) if run_points_data else None start_date = datetime.utcfromtimestamp(start_time / 1000) @@ -133,7 +147,7 @@ def parse_raw_data_to_nametuple( "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": run_data["distance"], - "average_heartrate": int(heart_rate) if heart_rate else None, + "average_heartrate": int(avg_heart_rate) if avg_heart_rate else None, "map": run_map(polyline_str), "start_latlng": start_latlng, "distance": run_data["distance"], @@ -172,18 +186,34 @@ def get_all_keep_tracks(email, password, old_tracks_ids, with_download_gpx=False def parse_points_to_gpx(run_points_data, start_time): - # future to support heart rate + """ + Convert run points data to GPX format. + + Args: + run_id (str): The ID of the run. + run_points_data (list of dict): A list of run data points. + start_time (int): The start time for adjusting timestamps. Note that the unit of the start_time is millsecond + + Returns: + gpx_data (str): GPX data in string format. + """ points_dict_list = [] + # early timestamp fields in keep's data stands for delta time, but in newly data timestamp field stands for exactly time, + # so it does'nt need to plus extra start_time + if run_points_data[0]["timestamp"] > TIMESTAMP_THRESHOLD_IN_DECISECOND: + start_time = 0 + for point in run_points_data: points_dict = { "latitude": point["latitude"], "longitude": point["longitude"], "time": datetime.utcfromtimestamp( - (point["timestamp"] * 100 + start_time) / 1000 + (point["timestamp"] * 100 + start_time) + / 1000 # note that the timestamp of a point is decisecond(分秒) ), + "elevation": point.get("verticalAccuracy"), + "hr": point.get("hr"), } - if "verticalAccuracy" in point: - points_dict["elevation"] = point["verticalAccuracy"] points_dict_list.append(points_dict) gpx = gpxpy.gpx.GPX() gpx.nsmap["gpxtpx"] = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1" @@ -195,12 +225,67 @@ def parse_points_to_gpx(run_points_data, start_time): gpx_segment = gpxpy.gpx.GPXTrackSegment() gpx_track.segments.append(gpx_segment) for p in points_dict_list: - point = gpxpy.gpx.GPXTrackPoint(**p) + point = gpxpy.gpx.GPXTrackPoint( + latitude=p["latitude"], + longitude=p["longitude"], + time=p["time"], + elevation=p.get("elevation"), + ) + if p.get("hr") is not None: + gpx_extension_hr = ET.fromstring( + f""" + {p["hr"]} + + """ + ) + point.extensions.append(gpx_extension_hr) gpx_segment.points.append(point) - return gpx.to_xml() +def find_nearest_hr( + hr_data_list, target_time, start_time, threshold=HR_FRAME_THRESHOLD_IN_DECISECOND +): + """ + Find the nearest heart rate data point to the target time. + if cannot found suitable HR data within the specified time frame (within 10 seconds by default), there will be no hr data return + Args: + heart_rate_data (list of dict): A list of heart rate data points, where each point is a dictionary + containing at least "timestamp" and "beatsPerMinute" keys. + target_time (float): The target timestamp for which to find the nearest heart rate data point. Please Note that the unit of target_time is decisecond(分秒), + means 1/10 of a second ,this is very unsual!! so when we convert a target_time to second we need to divide by 10, and when we convert a target time to millsecond + we need to times 100. + start_time (float): The reference start time. the unit of start_time is normal millsecond timestamp + threshold (float, optional): The maximum allowed time difference to consider a data point as the nearest. + Default is HR_THRESHOLD, the unit is decisecond(分秒) + + Returns: + int or None: The heart rate value of the nearest data point, or None if no suitable data point is found. + """ + closest_element = None + # init difference value + min_difference = float("inf") + if target_time > TIMESTAMP_THRESHOLD_IN_DECISECOND: + target_time = ( + target_time * 100 - start_time + ) / 100 # note that the unit of target_time is decisecond(分秒) and the unit of start_time is normal millsecond + + for item in hr_data_list: + timestamp = item["timestamp"] + difference = abs(timestamp - target_time) + + if difference <= threshold and difference < min_difference: + closest_element = item + min_difference = difference + + if closest_element: + hr = closest_element.get("beatsPerMinute") + if hr and hr > 0: + return hr + + return None + + def download_keep_gpx(gpx_data, keep_id): try: print(f"downloading keep_id {str(keep_id)} gpx") From 2ff0529b3829c34270a3d34825c1783459680b7c Mon Sep 17 00:00:00 2001 From: Ben Bell Date: Sun, 29 Oct 2023 21:54:04 +0800 Subject: [PATCH 03/11] feat: reduce grid svg file size (#535) --- run_page/gpxtrackposter/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/run_page/gpxtrackposter/utils.py b/run_page/gpxtrackposter/utils.py index 70e6e0c3d2f..b2234c97484 100644 --- a/run_page/gpxtrackposter/utils.py +++ b/run_page/gpxtrackposter/utils.py @@ -58,9 +58,13 @@ def project( scale = size.x / d_x if size.x / size.y <= d_x / d_y else size.y / d_y offset = offset + 0.5 * (size - scale * XY(d_x, -d_y)) - scale * XY(min_x, min_y) lines = [] + # If len > $zoom_threshold, choose 1 point out of every $step to reduce size of the SVG file + zoom_threshold = 400 for latlngline in latlnglines: line = [] - for latlng in latlngline: + step = int(len(latlngline) / zoom_threshold) + 1 + for i in range(0, len(latlngline), step): + latlng = latlngline[i] if bbox.contains(latlng): line.append((offset + scale * latlng2xy(latlng)).tuple()) else: From ccb1db176219ceee61df6ba84e9f3d7e8e62c18f Mon Sep 17 00:00:00 2001 From: haowei93 <727171008@qq.com> Date: Fri, 3 Nov 2023 20:01:21 +0800 Subject: [PATCH 04/11] upd deploy to github-pages (#537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 更新部署 github-pages 说明 * Update Deploy to Github-pages --------- Co-authored-by: haowei.chen --- README-CN.md | 7 +++++++ README.md | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/README-CN.md b/README-CN.md index 23f11e1518a..80dd71700c5 100644 --- a/README-CN.md +++ b/README-CN.md @@ -906,6 +906,13 @@ 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,需要做三点 + +- 修改你的 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: ''` 即可 + ## GitHub Actions diff --git a/README.md b/README.md index d0d21176c13..465d07207d4 100644 --- a/README.md +++ b/README.md @@ -720,6 +720,14 @@ 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: + +- 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: '' + ## GitHub Actions From 875f87aed262dabc409f01b3ef2d9d3ee8f84b8b Mon Sep 17 00:00:00 2001 From: Ben Bell Date: Mon, 6 Nov 2023 18:59:45 +0800 Subject: [PATCH 05/11] fix: gh-pages build: drop the ref while trigger from Run Data Sync (#540) * fix: gh-pages build: drop the ref while trigger from Run Data Sync --- .github/workflows/gh-pages.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 6850b560885..0adc1109124 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -47,6 +47,9 @@ jobs: steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v3 + 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 From cee8e1868942e76bf0c2ae659e6b0413b853f5e7 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sat, 11 Nov 2023 21:21:54 +0800 Subject: [PATCH 06/11] fix: nrc sync Signed-off-by: yihong0618 --- README-CN.md | 7 +++---- README.md | 7 +++---- run_page/config.py | 7 +++---- run_page/nike_sync.py | 18 ++++++++++++++---- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/README-CN.md b/README-CN.md index 80dd71700c5..114b85ea862 100644 --- a/README-CN.md +++ b/README-CN.md @@ -580,10 +580,9 @@ python3(python) run_page/garmin_sync.py xxxxxxxxxx --is-cn --only-run 获取 Nike 的 refresh_token -1. 登录 [Nike](https://www.nike.com) 官网 -2. In Developer -> Application-> Storage -> https:unite.nike.com 中找到 refresh_token - -![image](https://user-images.githubusercontent.com/15976103/94448123-23812b00-01dd-11eb-8143-4b0839c31d90.png) 3. 在项目根目录下执行: +**全部需要在大陆以外的全局 ip 下进行** +1. 在这里登陆[website](https://unite.nike.com/s3/unite/mobile.html?androidSDKVersion=3.1.0&corsoverride=https%3A%2F%2Funite.nike.com&uxid=com.nike.sport.running.droid.3.8&backendEnvironment=identity&view=login&clientId=VhAeafEGJ6G8e9DxRUz8iE50CZ9MiJMG), 打开 F12 在浏览器抓 login -> XHR -> get the `refresh_token` from login api +2. 复制 `refresh_token` 用 ```bash python3(python) run_page/nike_sync.py ${nike refresh_token} diff --git a/README.md b/README.md index 465d07207d4..3a2eba8f214 100644 --- a/README.md +++ b/README.md @@ -381,13 +381,12 @@ python3(python) run_page/garmin_sync.py xxxxxxxxxxxxxx(secret_string) --is-cn - Get Nike's `refresh_token` -1. Login [Nike](https://www.nike.com) website -2. In Develop -> Application-> Storage -> https:unite.nike.com look for `refresh_token` +**ALL need to do outside GFW** +1. Login from this [website](https://unite.nike.com/s3/unite/mobile.html?androidSDKVersion=3.1.0&corsoverride=https%3A%2F%2Funite.nike.com&uxid=com.nike.sport.running.droid.3.8&backendEnvironment=identity&view=login&clientId=VhAeafEGJ6G8e9DxRUz8iE50CZ9MiJMG), open F12 -> XHR -> get the `refresh_token` from login api +2. copy this `refresh_token` and use it
-![image](https://user-images.githubusercontent.com/15976103/94448123-23812b00-01dd-11eb-8143-4b0839c31d90.png) - 3. Execute in the root directory: ```bash diff --git a/run_page/config.py b/run_page/config.py index cd40767a31a..bae7be41597 100644 --- a/run_page/config.py +++ b/run_page/config.py @@ -20,10 +20,9 @@ JSON_FILE = os.path.join(parent, "src", "static", "activities.json") SYNCED_FILE = os.path.join(parent, "imported.json") -# TODO: Move into nike_sync -BASE_URL = "https://api.nike.com/sport/v3/me" -TOKEN_REFRESH_URL = "https://unite.nike.com/tokenRefresh" -NIKE_CLIENT_ID = "HlHa2Cje3ctlaOqnxvgZXNaAs7T9nAuH" +# TODO: Move into nike_sync NRC THINGS + + BASE_TIMEZONE = "Asia/Shanghai" diff --git a/run_page/nike_sync.py b/run_page/nike_sync.py index d2adfbd34ab..04c72f55e3d 100644 --- a/run_page/nike_sync.py +++ b/run_page/nike_sync.py @@ -1,4 +1,5 @@ import argparse +from base64 import b64decode import json import logging import os.path @@ -11,13 +12,10 @@ import httpx from config import ( BASE_TIMEZONE, - BASE_URL, GPX_FOLDER, JSON_FILE, - NIKE_CLIENT_ID, OUTPUT_DIR, SQL_FILE, - TOKEN_REFRESH_URL, run_map, ) from generator import Generator @@ -26,6 +24,16 @@ # logging.basicConfig(level=logging.INFO) logger = logging.getLogger("nike_sync") +BASE_URL = "https://api.nike.com/sport/v3/me" +TOKEN_REFRESH_URL = "https://api.nike.com/idn/shim/oauth/2.0/token" +NIKE_CLIENT_ID = "VmhBZWFmRUdKNkc4ZTlEeFJVejhpRTUwQ1o5TWlKTUc=" +NIKE_UX_ID = "Y29tLm5pa2Uuc3BvcnQucnVubmluZy5pb3MuNS4xNQ==" +NIKE_HEADERS = { + "Host": "api.nike.com", + "Accept": "application/json", + "Content-Type": "application/json", +} + class Nike: def __init__(self, refresh_token): @@ -33,10 +41,12 @@ def __init__(self, refresh_token): response = self.client.post( TOKEN_REFRESH_URL, + headers=NIKE_HEADERS, json={ "refresh_token": refresh_token, - "client_id": NIKE_CLIENT_ID, + "client_id": b64decode(NIKE_CLIENT_ID).decode(), "grant_type": "refresh_token", + "ux_id": b64decode(NIKE_UX_ID).decode(), }, timeout=60, ) From 0a99e20541d84fc71f1420d09ea4843dc83c8947 Mon Sep 17 00:00:00 2001 From: Zion <67903793+zlog-in@users.noreply.github.com> Date: Sun, 12 Nov 2023 14:41:02 +0100 Subject: [PATCH 07/11] Master (#545) * Update run_data_sync.yml * Update run_data_sync.yml * Update run_data_sync.yml * add screenshot to show how to get nike refresh token * add screenshot to show how to get nike refresh token * refactor --- README-CN.md | 6 +++++- README.md | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README-CN.md b/README-CN.md index 114b85ea862..6b1d88c9195 100644 --- a/README-CN.md +++ b/README-CN.md @@ -581,8 +581,12 @@ python3(python) run_page/garmin_sync.py xxxxxxxxxx --is-cn --only-run 获取 Nike 的 refresh_token **全部需要在大陆以外的全局 ip 下进行** + +![example img](https://user-images.githubusercontent.com/67903793/282300381-4e7437d0-65a9-4eed-93d1-2b70e360215f.png) + 1. 在这里登陆[website](https://unite.nike.com/s3/unite/mobile.html?androidSDKVersion=3.1.0&corsoverride=https%3A%2F%2Funite.nike.com&uxid=com.nike.sport.running.droid.3.8&backendEnvironment=identity&view=login&clientId=VhAeafEGJ6G8e9DxRUz8iE50CZ9MiJMG), 打开 F12 在浏览器抓 login -> XHR -> get the `refresh_token` from login api -2. 复制 `refresh_token` 用 + +2. 复制 `refresh_token` 之后可以添加在GitHub Secrets 中,也可以直接在命令行中使用 ```bash python3(python) run_page/nike_sync.py ${nike refresh_token} diff --git a/README.md b/README.md index 3a2eba8f214..740a1a1de26 100644 --- a/README.md +++ b/README.md @@ -382,10 +382,12 @@ python3(python) run_page/garmin_sync.py xxxxxxxxxxxxxx(secret_string) --is-cn - Get Nike's `refresh_token` **ALL need to do outside GFW** -1. Login from this [website](https://unite.nike.com/s3/unite/mobile.html?androidSDKVersion=3.1.0&corsoverride=https%3A%2F%2Funite.nike.com&uxid=com.nike.sport.running.droid.3.8&backendEnvironment=identity&view=login&clientId=VhAeafEGJ6G8e9DxRUz8iE50CZ9MiJMG), open F12 -> XHR -> get the `refresh_token` from login api -2. copy this `refresh_token` and use it -
+![example img](https://user-images.githubusercontent.com/67903793/282300381-4e7437d0-65a9-4eed-93d1-2b70e360215f.png) + +1. Login from this [website](https://unite.nike.com/s3/unite/mobile.html?androidSDKVersion=3.1.0&corsoverride=https%3A%2F%2Funite.nike.com&uxid=com.nike.sport.running.droid.3.8&backendEnvironment=identity&view=login&clientId=VhAeafEGJ6G8e9DxRUz8iE50CZ9MiJMG), open F12 -> XHR -> get the `refresh_token` from login api. + +2. copy this `refresh_token` and use it in GitHub Secrets or in command line 3. Execute in the root directory: From 0911ed89e1ed9fe21e460f8c189cab9d225b24b5 Mon Sep 17 00:00:00 2001 From: Li Peng Date: Mon, 13 Nov 2023 18:19:19 +0800 Subject: [PATCH 08/11] Update strava_to_garmin_sync.py (#544) Sort the activities downloaded from Strava by their id's thus they are uploaded to Garmin from the oldest to the latest. Otherwise, the latest one is uploaded first, and if the process is interrupted, any activities older than it will never be uploaded because the code before this line gets the latest activity from Garmin and only download those newer than it from Strava. --- run_page/strava_to_garmin_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_page/strava_to_garmin_sync.py b/run_page/strava_to_garmin_sync.py index 41a786136b3..34adba7d7a5 100644 --- a/run_page/strava_to_garmin_sync.py +++ b/run_page/strava_to_garmin_sync.py @@ -92,7 +92,7 @@ async def upload_to_activities( return files_list # strava rate limit - for i in strava_activities[: len(strava_activities)]: + for i in sorted(strava_activities, key=lambda i: int(i.id)): try: data = strava_web_client.get_activity_data(i.id, fmt=format) files_list.append(data) From 9727d594f67133fff1cde39e2217e200902a0bc2 Mon Sep 17 00:00:00 2001 From: yihong0618 Date: Sat, 18 Nov 2023 22:49:21 +0800 Subject: [PATCH 09/11] doc: add nrc detail written by @angolap --- README-CN.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README-CN.md b/README-CN.md index 6b1d88c9195..cb687deac0e 100644 --- a/README-CN.md +++ b/README-CN.md @@ -588,6 +588,8 @@ python3(python) run_page/garmin_sync.py xxxxxxxxxx --is-cn --only-run 2. 复制 `refresh_token` 之后可以添加在GitHub Secrets 中,也可以直接在命令行中使用 +> Chrome 浏览器:按下 F12 打开浏览器开发者工具,点击 Application 选项卡,来到左侧的 Storage 面板,点击展开 Local storage,点击下方的 https://unite.nike.com。接着点击右侧的 com.nike.commerce.nikedotcom.web.credential Key,下方会分行显示我们选中的对象,可以看到 refresh_token ,复制 refresh_token 右侧的值。Safari 浏览器:在 Safari 打开 Nike 的网页后,右击页面,选择「检查元素」,打开浏览器开发者工具。点击「来源」选项卡,在左侧找到 XHR 文件夹,点击展开,在下方找到 login 文件并单击,在右侧同样可以看到 refresh_token ,复制 refresh_token 右侧的值。 + ```bash python3(python) run_page/nike_sync.py ${nike refresh_token} ``` From dbdfa8c672114c4750dae5017839a6932b9d078d Mon Sep 17 00:00:00 2001 From: orionna319 <85230052+orionna319@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:51:08 +0800 Subject: [PATCH 10/11] perf: use auto instead of scroll (#550) Co-authored-by: caojiawen --- src/components/RunTable/style.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RunTable/style.module.scss b/src/components/RunTable/style.module.scss index 50307bf3888..997bc7b3c9b 100644 --- a/src/components/RunTable/style.module.scss +++ b/src/components/RunTable/style.module.scss @@ -47,7 +47,7 @@ .tableContainer { width: 100%; - overflow-x: scroll; + overflow-x: auto; } .runDate { From 048e941a9d4fe5e403e94ca6073eddcd52f1d6db Mon Sep 17 00:00:00 2001 From: orionna319 <85230052+orionna319@users.noreply.github.com> Date: Fri, 24 Nov 2023 09:58:08 +0800 Subject: [PATCH 11/11] feat: `RunRow` toggle selection & cursor pointer (#551) --- src/components/RunTable/RunRow.tsx | 10 +++++++--- src/components/RunTable/index.tsx | 3 ++- src/components/RunTable/style.module.scss | 1 + src/pages/index.tsx | 7 +++++-- src/utils/utils.ts | 2 ++ 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/RunTable/RunRow.tsx b/src/components/RunTable/RunRow.tsx index 003ae3a2e28..63afada7d05 100644 --- a/src/components/RunTable/RunRow.tsx +++ b/src/components/RunTable/RunRow.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { formatPace, titleForRun, formatRunTime, Activity } from '@/utils/utils'; +import { formatPace, titleForRun, formatRunTime, Activity, RunIds } from '@/utils/utils'; import styles from './style.module.scss'; interface IRunRowProperties { elementIndex: number; - locateActivity: (_date: string) => void; + locateActivity: (_runIds: RunIds) => void; run: Activity; runIndex: number; setRunIndex: (_ndex: number) => void; @@ -16,7 +16,11 @@ const RunRow = ({ elementIndex, locateActivity, run, runIndex, setRunIndex }: IR const heartRate = run.average_heartrate; const runTime = formatRunTime(run.moving_time); const handleClick = () => { - if (runIndex === elementIndex) return; + if (runIndex === elementIndex) { + setRunIndex(-1); + locateActivity([]); + return + }; setRunIndex(elementIndex); locateActivity([run.run_id]); }; diff --git a/src/components/RunTable/index.tsx b/src/components/RunTable/index.tsx index 64b2b7910a6..c6c8f0a3429 100644 --- a/src/components/RunTable/index.tsx +++ b/src/components/RunTable/index.tsx @@ -4,13 +4,14 @@ import { sortDateFuncReverse, convertMovingTime2Sec, Activity, + RunIds, } from '@/utils/utils'; import RunRow from './RunRow'; import styles from './style.module.scss'; interface IRunTableProperties { runs: Activity[]; - locateActivity: (_date: string) => void; + locateActivity: (_runIds: RunIds) => void; setActivity: (_runs: Activity[]) => void; runIndex: number; setRunIndex: (_index: number) => void; diff --git a/src/components/RunTable/style.module.scss b/src/components/RunTable/style.module.scss index 997bc7b3c9b..51be0b330b4 100644 --- a/src/components/RunTable/style.module.scss +++ b/src/components/RunTable/style.module.scss @@ -26,6 +26,7 @@ } .runRow { + cursor: pointer; td { padding: 0.5rem; border: 0; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ec8ce045b8e..d38e78ae460 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -21,6 +21,7 @@ import { scrollToMap, sortDateFunc, titleForShow, + RunIds, } from '@/utils/utils'; const Index = () => { @@ -74,10 +75,12 @@ const Index = () => { changeByItem(title, 'Title', filterTitleRuns); }; - const locateActivity = (runIds: [Number]) => { + const locateActivity = (runIds: RunIds) => { const ids = new Set(runIds); - const selectedRuns = runs.filter((r) => ids.has(r.run_id)); + const selectedRuns = !runIds.length + ? runs + : runs.filter((r: any) => ids.has(r.run_id)); if (!selectedRuns.length) { return; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 37a9795da3f..80d5d157327 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -8,6 +8,8 @@ import { FeatureCollection, LineString } from 'geojson'; export type Coordinate = [number, number]; +export type RunIds = Array | []; + export interface Activity { run_id: number; name: string;