From 8f1f03fc15c0aef02e82e59448998883c40ed0c5 Mon Sep 17 00:00:00 2001 From: Valentin M Date: Sun, 24 Jul 2022 14:08:46 +0000 Subject: [PATCH 1/7] added new medium widget that displays weekly stats --- strava.js | 347 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 298 insertions(+), 49 deletions(-) diff --git a/strava.js b/strava.js index 2960ec7..c6bdd39 100644 --- a/strava.js +++ b/strava.js @@ -5,7 +5,7 @@ // ############################################# // ### Strava Scriptable Widget by @dwd0tcom ### // ############################################# -// ############# Version 1.0.0 ################# +// ############# Version 1.1.0 ################# // ############################################# // // Changelog: @@ -13,6 +13,10 @@ // – Added support for Strava's highlight image // — Better error handling // — Added offline fallback for image + json data +// V 1.1.0 +// - added medium widget for three sport types (triathlon) +// - added support to run the script from inside the app +// TODO: better icons / original icons from strava // // ############################################# // ############################################# @@ -26,65 +30,49 @@ // // Set to false if you don't want to see a photo const photoWidget = true +const MAX_Y_HEIGHT = 75; // for medium widget // // ############################################# // ############################################# -let clientID, clientSecret, refreshToken, data, miles, kmh, name, activityId, img -const callActivities = `https://www.strava.com/api/v3/athlete/activities?access_token=` -let widgetInput = args.widgetParameter - -if (widgetInput !== null) { - [clientID, clientSecret, refreshToken] = widgetInput.split("|"); - - if (!clientID || !clientSecret || !refreshToken) { - throw new Error("Invalid parameter. Expected format: clientID|ClientSecret|RefreshToken") - } - -} else { - throw new Error("No parameters set. Please insert your paremeters like this: clientID|ClientSecret|RefreshToken") -} - -const apiURL = (clientID, clientSecret, refreshToken) => `https://www.strava.com/oauth/token?client_id=${clientID}&client_secret=${clientSecret}&refresh_token=${refreshToken}&grant_type=refresh_token` - +// function definitions for data processing and small widget const saveStravaData = (data) => { - let fm = FileManager.iCloud(); - let path = fm.joinPath( fm.documentsDirectory(), 'strava-data.json' ); - fm.writeString(path, JSON.stringify(data)); + let fm = FileManager.iCloud(); + let path = fm.joinPath(fm.documentsDirectory(), 'strava-data.json'); + fm.writeString(path, JSON.stringify(data)); }; const saveImage = (img) => { - let fm = FileManager.iCloud(); - let path = fm.joinPath( fm.documentsDirectory(), 'strava-image.jpg' ); - fm.writeImage(path, img); + let fm = FileManager.iCloud(); + let path = fm.joinPath(fm.documentsDirectory(), 'strava-image.jpg'); + fm.writeImage(path, img); }; -const getSavedStravaData = () => { - let fm = FileManager.iCloud(); - let path = fm.joinPath(fm.documentsDirectory(), 'strava-data.json'); - let data = fm.readString( path ); - return JSON.parse(data); +const getSavedStravaData = (filename) => { + filename = filename ? filename : 'strava-data.json' + let fm = FileManager.iCloud(); + let path = fm.joinPath(fm.documentsDirectory(), filename); + let data = fm.readString(path); + return JSON.parse(data); }; const getSavedImage = () => { - let fm = FileManager.iCloud(); - let path = fm.joinPath(fm.documentsDirectory(), 'strava-image.jpg'); - let img = fm.readImage( path ); - return img; + let fm = FileManager.iCloud(); + let path = fm.joinPath(fm.documentsDirectory(), 'strava-image.jpg'); + let img = fm.readImage(path); + return img; }; -let latestActivity = await loadActivity(clientID, clientSecret, refreshToken) -let widget = await createWidget(latestActivity) -widget.url = "strava://feed" +const getLastMonday = () => { + var prevMonday = new Date(); + prevMonday.setDate(prevMonday.getDate() - (prevMonday.getDay() + 6) % 7); + prevMonday.setHours(0, 0, 0) + parseInt(prevMonday.getTime() / 1000) -if (!config.runsInWidget) { - await widget.presentSmall() + return ({ MondayAsInt: parseInt(prevMonday.getTime() / 1000), prevMonday: prevMonday }) } -Script.setWidget(widget) -Script.complete() - -async function loadActivity(clientID, clientSecret, refreshToken) { +async function loadActivity(clientID, clientSecret, refreshToken, numberOfActivities) { try { const req = new Request(apiURL(clientID, clientSecret, refreshToken)) req.method = "POST" @@ -92,15 +80,15 @@ async function loadActivity(clientID, clientSecret, refreshToken) { const accessToken = response.access_token // Get data of latest activity, in this case just the ID - const dataComplete = await new Request(callActivities + accessToken + "&per_page=1").loadJSON() + const dataComplete = await new Request(callActivities + accessToken + `&per_page=${numberOfActivities}`).loadJSON() const activityId = dataComplete[0].id // Get latest activity, complete dataset for images. Kinda annyoing... const callSingleActivity = `https://www.strava.com/api/v3/activities/` let data = await new Request(callSingleActivity + activityId + "?access_token=" + accessToken).loadJSON() - // Save file to local saveStravaData(data) + console.log('using online data') return data @@ -140,7 +128,7 @@ async function createWidget(data) { let bg3; let textColor; - if(hasPhoto > 0) { + if (hasPhoto > 0) { textColor = new Color('#ffffff') } else { textColor = Color.dynamic(new Color('#ffffff'), new Color('#000000')); @@ -161,7 +149,7 @@ async function createWidget(data) { // Get latest Distance const latestDistance = data.distance let num = (latestDistance / 1000).toString() - let roundedDistance = num.slice(0, (num.indexOf("."))+3) + let roundedDistance = num.slice(0, (num.indexOf(".")) + 3) let distance = detailsStackFirstRow.addText(roundedDistance + " km") distance.font = Font.mediumSystemFont(10) @@ -182,7 +170,7 @@ async function createWidget(data) { // Get max speed const maxSpeedData = data.max_speed let maxSpeed = milesToKm(maxSpeedData).toFixed(1) - let maxSpeedText = detailsStackSecondRow.addText("Max. " + maxSpeed + " km/h") + let maxSpeedText = detailsStackSecondRow.addText("Max. " + maxSpeed + " km/h") maxSpeedText.font = Font.mediumSystemFont(10) maxSpeedText.textColor = textColor @@ -206,7 +194,7 @@ async function createWidget(data) { kudosImageInline.imageSize = new Size(10, 10) kudosImageInline.tintColor = textColor - if(hasPhoto > 0 && photoWidget) { + if (hasPhoto > 0 && photoWidget) { const images = data.photos; //Get a photo from the activity @@ -214,7 +202,7 @@ async function createWidget(data) { try { img = await loadImage(imgUrl) - saveImage (img) + saveImage(img) console.log('using online image') } catch (e) { img = getSavedImage() @@ -260,3 +248,264 @@ async function loadImage(imgUrl) { let imgReq = new Request(imgUrl) return await imgReq.loadImage() } + +async function loadActivityFromLastNDays(clientID, clientSecret, refreshToken, numberOfActivities, after) { + + try { + const req = new Request(apiURL(clientID, clientSecret, refreshToken)) + req.method = "POST" + let response = await req.loadJSON() + const accessToken = response.access_token + // Get data of latest activity, in this case just the ID + const dataComplete = await new Request(callActivities + accessToken + `&per_page=${numberOfActivities}` + `&after=${after}`).loadJSON() + let summary = createSummaryFromActivies(dataComplete); + + const activityId = dataComplete[0].id + + // Get latest activity, complete dataset for images. Kinda annyoing... + const callSingleActivity = `https://www.strava.com/api/v3/activities/` + let latestActivity = await new Request(callSingleActivity + activityId + "?access_token=" + accessToken).loadJSON() + // Save file to local + saveStravaData({summary:summary, latestActivity:latestActivity}) + console.log('using online latestActivityData') + return({summary:summary, latestActivity:latestActivity}) + + } catch (e) { + // If API is offline, use local data + // TODO get saved summary data as well + const offlineData = getSavedStravaData(); + console.log('using saved/offline data') + return(offlineData) + + } +} + +function getMinPerKilometer(moving_time_in_seconds, distance_in_meters) { + let runPace = ((moving_time_in_seconds / 60) / (distance_in_meters / 1000)) + let runPaceStr = runPace ? `${String(parseInt(runPace)).padStart(2, '0')}:${((runPace % 1) * 60).toFixed(0).padStart(2, '0')}` : '00:00' + return ({ runPace, runPaceStr }) +} + +function getKilometersPerHour(moving_time_in_seconds, distance_in_meters) { + let kmh = (distance_in_meters / 1000) / (moving_time_in_seconds / (60 * 60)) + return ({ kmh: kmh, kmhStr: kmh ? kmh.toFixed(2) : '00:00' }) +} + +function getPacePer100m(moving_time_in_seconds, distance_in_meters) { + let swimPace = ((moving_time_in_seconds / 60) / (distance_in_meters / 100)) + let swimPaceStr = swimPace ? `${String(parseInt(swimPace)).padStart(2, '0')}:${((swimPace % 1) * 60).toFixed(0).padStart(2, '0')}` : '00:00' + return ({ swimPace, swimPaceStr }) +} + +function createSummaryFromActivies(activities) { + + // 1) CREATE SUMMARY FOR CURRENT WEEK + let initialValue = 0 + let total_seconds_swim = activities.filter((act) => act.type == 'Swim').reduce((act, prevActs) => act + prevActs.moving_time, initialValue) + let total_seconds_bike = activities.filter((act) => act.type == 'Ride').reduce((act, prevActs) => act + prevActs.moving_time, initialValue) + let total_seconds_run = activities.filter((act) => act.type == 'Run').reduce((act, prevActs) => act + prevActs.moving_time, initialValue) + let total_seconds_other = activities.filter((act) => act.type != "Run" && act.type != "Ride" && act.type != "Swim").reduce((act, prevActs) => act + prevActs.moving_time, initialValue) + + let total_meters_swim = activities.filter((act) => act.type == 'Swim').reduce((act, prevActs) => act + prevActs.distance, initialValue) + let total_meters_bike = activities.filter((act) => act.type == 'Ride').reduce((act, prevActs) => act + prevActs.distance, initialValue) + let total_meters_run = activities.filter((act) => act.type == 'Run').reduce((act, prevActs) => act + prevActs.distance, initialValue) + let total_meters_other = activities.filter((act) => act.type != "Run" && act.type != "Ride" && act.type != "Swim").reduce((act, prevActs) => act + prevActs.distance, initialValue) + + // 2) CREATE DAILY SUMMARIES + // create summary for each day in the current week + // new Date().getDay() method starts with sunday with index = 0 -> Monday has index 1 + const weekLabels = ['S', 'M', 'D', 'M', 'D', 'F', 'S'] + const daily_summary = [] + for (let dayIndex = 0; dayIndex < weekLabels.length; dayIndex++) { + let weekLabel = weekLabels[dayIndex]; + let actsPerDay = activities.filter((act) => new Date(act.start_date_local).getDay() == dayIndex) + let daily_total_seconds = actsPerDay.reduce((act, prevActs) => act + prevActs.moving_time, 0) + let daily_total_distance = actsPerDay.reduce((act, prevActs) => act + prevActs.distance, 0) + let daily_total_sports = actsPerDay.map(act => act.type) + + let daily_sport_type_label; + if(daily_total_sports[0]){ + daily_sport_type_label = (new Set(daily_total_sports).size) == 1 && daily_total_sports[0] ? daily_total_sports[0] : 'Multi' + } + + daily_summary.push( + { + dayIndexUS: dayIndex, + dayIndexDE: dayIndex == 0 ? 6 : dayIndex-1, + weekLabel: weekLabel, + dailyActivityCount: actsPerDay.length, + daily_total_seconds: daily_total_seconds, + daily_total_distance: daily_total_distance, + daily_sport_type_label: daily_sport_type_label + }) + } + + return ({ + total_seconds_swim: total_seconds_swim, + total_seconds_bike: total_seconds_bike, + total_seconds_run: total_seconds_run, + total_seconds_other: total_seconds_other, + + total_meters_swim: total_meters_swim, + total_meters_bike: total_meters_bike, + total_meters_run: total_meters_run, + total_meters_other: total_meters_other, + + mean_pace_swim: getPacePer100m(total_seconds_swim, total_meters_swim), + mean_pace_bike: getKilometersPerHour(total_seconds_bike, total_meters_bike), + mean_pace_run: getMinPerKilometer(total_seconds_run, total_meters_run), + + daily_summary: daily_summary + }) + +} + +// function definitions for medium widget +const normSummaryDataTo100Percent = (summary, MAX_Y) => { + let maxSeconds = Math.max(...summary.daily_summary.map((a) => a.daily_total_seconds)) + let scale_factor = MAX_Y/maxSeconds; + summary.daily_summary = summary.daily_summary.map( d => ({...d, scaled_daily_total_seconds: d.daily_total_seconds*scale_factor})); + return(summary); +} + +function createProgressBar(context, dayIndex, dayLabel, total_hrs_percent, sportType) { + if (total_hrs_percent > 0) { + context.setFillColor(new Color("#f7551f")) + } else { + context.setFillColor(new Color("#000")) + total_hrs_percent = 1 //1% + } + + const path = new Path() + let rect_h = total_hrs_percent// = activity_hrs + let rect_w = 15 // breite des bars + let x = 105 + ((dayIndex + 1) * 25) + let y = 100 - total_hrs_percent + path.addRect(new Rect(x, y, rect_w, rect_h), 30, 20) + context.addPath(path) + context.fillPath() + context.drawText(dayLabel, new Point(x + 1.25, y + rect_h + 1)) + if (sportType == 'Run') { + let symRun = SFSymbol.named('figure.walk').image + context.drawImageAtPoint(symRun, new Point(x, y - 25)) + } else if (sportType == 'Ride') { + const symBike = SFSymbol.named('bicycle').image + context.drawImageAtPoint(symBike, new Point(x - 7.5, y - 25)) + } else if (sportType == 'Swim') { + const symSwim = SFSymbol.named('rays').image + context.drawImageAtPoint(symSwim, new Point(x - 2.5, y - 25)) + } else if (total_hrs_percent > 1) { + //TODO: other sports... + } +} + +function mainCreateMediumWidget(stravaScaledSummaryData) { + const DEVICE_LANGUAGE = Device.language(); + const scaledSummaryData = stravaScaledSummaryData + + // init context and widget + const w = new ListWidget() + w.backgroundColor = new Color("#ffffff") + const context = new DrawContext() + context.size = new Size(300, 115) + context.opaque = false + context.respectScreenScale = true + + // A) print weekly stats as a summary on the left + context.setFont(Font.boldSystemFont(16)) + context.drawText(DEVICE_LANGUAGE == 'de' ? 'Statistik' : 'Statistics', new Point(0,-2.5)) + let rowY = -5 + // 1. Row: Swimming + context.setFont(Font.regularSystemFont(11)) + const symSwim = SFSymbol.named('rays').image + rowY += 25 + context.drawImageAtPoint(symSwim, new Point(0, rowY)) + context.drawText(`Σ ${scaledSummaryData.total_seconds_swim > 0 ? parseInt(`${scaledSummaryData.total_seconds_swim/60}`) : '-'} min`, new Point(32.5, rowY-2.5)) + context.drawText(`⌀ ${scaledSummaryData.mean_pace_swim.swimPaceStr} min/100m`,new Point(32.5, rowY + 12.5)) + + // 2. Row: Bike + context.setFont(Font.regularSystemFont(11)) + rowY += 35 + const symBike = SFSymbol.named('bicycle').image + context.drawImageAtPoint(symBike, new Point(0, rowY)) + context.drawText(`Σ ${scaledSummaryData.total_seconds_bike > 0 ? parseInt(`${scaledSummaryData.total_seconds_bike/60}`) : '-'} min`, new Point(32.5, rowY-2.5)) + context.drawText(`⌀ ${scaledSummaryData.mean_pace_bike.kmhStr} km/h`,new Point(32.5, rowY + 12.5)) + + // 3. Row: Run + context.setFont(Font.regularSystemFont(11)) + rowY += 35 + const symRun = SFSymbol.named('figure.walk').image + context.drawImageAtPoint(symRun, new Point(0, rowY)) + context.drawText(`Σ ${scaledSummaryData.total_seconds_run > 0 ? parseInt(`${scaledSummaryData.total_seconds_run/60}`) : '-'} min`, new Point(32.5, rowY-2.5)) + context.drawText(`⌀ ${scaledSummaryData.mean_pace_run.runPaceStr} min/km`,new Point(32.5, rowY + 12.5)) + + + // B) make bar chart from daily statistics + context.setFont(Font.regularSystemFont(14)) + for (let index = 0; index < scaledSummaryData.daily_summary.length; index++) { + let day = scaledSummaryData.daily_summary[index]; + createProgressBar(context, DEVICE_LANGUAGE == 'de' ? day.dayIndexDE : day.dayIndexUS, day.weekLabel, day.scaled_daily_total_seconds, day.daily_sport_type_label ? day.daily_sport_type_label : '') + } + // finalize drawings + const img = context.getImage() + const imgw = w.addImage(img) + imgw.imageSize = new Size(300, 115) + + // run widget + Script.setWidget(w) + w.presentMedium() + Script.complete() +} + + +//// MAIN +let clientID, clientSecret, refreshToken, data, miles, kmh, name, activityId, img; +const callActivities = `https://www.strava.com/api/v3/athlete/activities?access_token=` +let widgetInput = args.widgetParameter + +if (widgetInput !== null) { + [clientID, clientSecret, refreshToken] = widgetInput.split("|"); + + if (!clientID || !clientSecret || !refreshToken) { + throw new Error("Invalid parameter. Expected format: clientID|ClientSecret|RefreshToken") + } + +} else if(!config.runsInWidget){ + console.log("accessing secrets from file..."); + // create that file manually inside the scriptable folder, to be able to run the script from inside the scriptable app + // strava-secret.json content: + // {"clientID": "", "clientSecret": "", "refreshToken": ""} + const {clientID, clientSecret, refreshToken} = getSavedStravaData('strava-secret.json') + if (!clientID || !clientSecret || !refreshToken) { + throw new Error('Invalid parameter. Expected text content: {"clientID": "", "clientSecret": "", "refreshToken": ""}') + } +} +else { + throw new Error("No parameters set. Please insert your parameters like this: clientID|ClientSecret|RefreshToken. Alternatively you can create a json file named 'strava-secret.json' inside the app folder") +} + +const apiURL = (clientID, clientSecret, refreshToken) => `https://www.strava.com/oauth/token?client_id=${clientID}&client_secret=${clientSecret}&refresh_token=${refreshToken}&grant_type=refresh_token` + +const numberOfActivities = 30 //always load 30 activities and the last one complete; config.widgetFamily != 'small' ? 30 : 1; // load only 1 activity for the small widget +const showLastNDays = 7 + +let {summary, latestActivity} = await loadActivityFromLastNDays(clientID, clientSecret, refreshToken, numberOfActivities, getLastMonday().MondayAsInt) +console.log(latestActivity); +console.log(summary); +// MAIN +// if(!config.runsInWidget){ +if(config.widgetFamily == 'small'){ + let widget = await createWidget(latestActivity) + widget.url = "strava://feed" + Script.setWidget(widget) + widget.presentSmall() + Script.complete() + console.log('this runs as small widget'); +} + +if(config.widgetFamily == 'medium' || !config.runsInWidget){ +// if(!config.runsInWidget){ + const scaledSummaryData = normSummaryDataTo100Percent(summary, MAX_Y_HEIGHT); + mainCreateMediumWidget(scaledSummaryData); + console.log('this runs as medium widget'); +} \ No newline at end of file From 2ac0cbf4342df886d7b4948fd5998fc66217a3d7 Mon Sep 17 00:00:00 2001 From: Valentin M Date: Sun, 24 Jul 2022 14:13:41 +0000 Subject: [PATCH 2/7] updated description for medium sized widget --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7462965..59cbe72 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,10 @@ Download and install **strava.js** to you and place the small widget on your hom - **Added offline fallback for image + json data** In some cases, when the celluar data is weak, you won't be able to see a picture, if the picture comes from an online source. Now, as a fallback, it's saved in iCloud. +#### Version 1.1.0 +- **Added new medium sized widget that shows you weekly statistics** + The medium sized widget will be automatically selected if you set up your size accordingly. + ## Troubleshooting If there is any error, it might get caused because the API endpoint is not available, when the widget tries to pull data from Strava. In order to check the API status, please visit [Strava Server Status](https://status.strava.com/#day). From aa4f3c7f60fa249535e2f61d4d24ec511df1eb18 Mon Sep 17 00:00:00 2001 From: Valentin M Date: Sun, 24 Jul 2022 14:18:47 +0000 Subject: [PATCH 3/7] changed swim icon --- strava.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strava.js b/strava.js index c6bdd39..6a4672d 100644 --- a/strava.js +++ b/strava.js @@ -392,7 +392,7 @@ function createProgressBar(context, dayIndex, dayLabel, total_hrs_percent, sport const symBike = SFSymbol.named('bicycle').image context.drawImageAtPoint(symBike, new Point(x - 7.5, y - 25)) } else if (sportType == 'Swim') { - const symSwim = SFSymbol.named('rays').image + const symSwim = SFSymbol.named('drop').image context.drawImageAtPoint(symSwim, new Point(x - 2.5, y - 25)) } else if (total_hrs_percent > 1) { //TODO: other sports... @@ -417,7 +417,7 @@ function mainCreateMediumWidget(stravaScaledSummaryData) { let rowY = -5 // 1. Row: Swimming context.setFont(Font.regularSystemFont(11)) - const symSwim = SFSymbol.named('rays').image + const symSwim = SFSymbol.named('drop').image rowY += 25 context.drawImageAtPoint(symSwim, new Point(0, rowY)) context.drawText(`Σ ${scaledSummaryData.total_seconds_swim > 0 ? parseInt(`${scaledSummaryData.total_seconds_swim/60}`) : '-'} min`, new Point(32.5, rowY-2.5)) From eb2e83a9a27e188e1a553479db21d722f3fb9d5b Mon Sep 17 00:00:00 2001 From: Valentin M Date: Sun, 24 Jul 2022 14:25:33 +0000 Subject: [PATCH 4/7] deleted unnecessary loadActivity function --- strava.js | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/strava.js b/strava.js index 6a4672d..16cb87d 100644 --- a/strava.js +++ b/strava.js @@ -72,36 +72,6 @@ const getLastMonday = () => { return ({ MondayAsInt: parseInt(prevMonday.getTime() / 1000), prevMonday: prevMonday }) } -async function loadActivity(clientID, clientSecret, refreshToken, numberOfActivities) { - try { - const req = new Request(apiURL(clientID, clientSecret, refreshToken)) - req.method = "POST" - let response = await req.loadJSON() - const accessToken = response.access_token - - // Get data of latest activity, in this case just the ID - const dataComplete = await new Request(callActivities + accessToken + `&per_page=${numberOfActivities}`).loadJSON() - const activityId = dataComplete[0].id - - // Get latest activity, complete dataset for images. Kinda annyoing... - const callSingleActivity = `https://www.strava.com/api/v3/activities/` - let data = await new Request(callSingleActivity + activityId + "?access_token=" + accessToken).loadJSON() - // Save file to local - saveStravaData(data) - - console.log('using online data') - - return data - - } catch (e) { - // If API is offline, use local data - data = getSavedStravaData(); - console.log('using saved data') - return data - - } -} - async function createWidget(data) { let milesToKm = (miles) => { From 58a17a70109136c54e2bf64fcf5afe1bea697b11 Mon Sep 17 00:00:00 2001 From: vmarquar Date: Sun, 24 Jul 2022 17:35:58 +0200 Subject: [PATCH 5/7] new swim icon --- strava.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/strava.js b/strava.js index 16cb87d..88cc279 100644 --- a/strava.js +++ b/strava.js @@ -362,7 +362,7 @@ function createProgressBar(context, dayIndex, dayLabel, total_hrs_percent, sport const symBike = SFSymbol.named('bicycle').image context.drawImageAtPoint(symBike, new Point(x - 7.5, y - 25)) } else if (sportType == 'Swim') { - const symSwim = SFSymbol.named('drop').image + const symSwim = SFSymbol.named('humidity').image context.drawImageAtPoint(symSwim, new Point(x - 2.5, y - 25)) } else if (total_hrs_percent > 1) { //TODO: other sports... @@ -387,7 +387,7 @@ function mainCreateMediumWidget(stravaScaledSummaryData) { let rowY = -5 // 1. Row: Swimming context.setFont(Font.regularSystemFont(11)) - const symSwim = SFSymbol.named('drop').image + const symSwim = SFSymbol.named('humidity').image rowY += 25 context.drawImageAtPoint(symSwim, new Point(0, rowY)) context.drawText(`Σ ${scaledSummaryData.total_seconds_swim > 0 ? parseInt(`${scaledSummaryData.total_seconds_swim/60}`) : '-'} min`, new Point(32.5, rowY-2.5)) @@ -445,7 +445,8 @@ if (widgetInput !== null) { // create that file manually inside the scriptable folder, to be able to run the script from inside the scriptable app // strava-secret.json content: // {"clientID": "", "clientSecret": "", "refreshToken": ""} - const {clientID, clientSecret, refreshToken} = getSavedStravaData('strava-secret.json') + const {clientID, clientSecret, refreshToken} = getSavedStravaData('strava-secret.json'); + if (!clientID || !clientSecret || !refreshToken) { throw new Error('Invalid parameter. Expected text content: {"clientID": "", "clientSecret": "", "refreshToken": ""}') } @@ -463,7 +464,6 @@ let {summary, latestActivity} = await loadActivityFromLastNDays(clientID, client console.log(latestActivity); console.log(summary); // MAIN -// if(!config.runsInWidget){ if(config.widgetFamily == 'small'){ let widget = await createWidget(latestActivity) widget.url = "strava://feed" From e993c9cc67fa6ea67da63a05f66451b733fff8b7 Mon Sep 17 00:00:00 2001 From: vmarquar Date: Tue, 26 Jul 2022 13:29:06 +0200 Subject: [PATCH 6/7] fixed file not downloaded from icloud error --- strava.js | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/strava.js b/strava.js index 88cc279..14f110c 100644 --- a/strava.js +++ b/strava.js @@ -31,11 +31,15 @@ // Set to false if you don't want to see a photo const photoWidget = true const MAX_Y_HEIGHT = 75; // for medium widget +const DISPLAY_SINGLE_ACTIVITY_AS_MEDIUM_WIDGET = false // set it to true, if you want to display the latest activity as a medium widget + // // ############################################# // ############################################# // function definitions for data processing and small widget +const apiURL = (clientID, clientSecret, refreshToken) => `https://www.strava.com/oauth/token?client_id=${clientID}&client_secret=${clientSecret}&refresh_token=${refreshToken}&grant_type=refresh_token` + const saveStravaData = (data) => { let fm = FileManager.iCloud(); let path = fm.joinPath(fm.documentsDirectory(), 'strava-data.json'); @@ -48,11 +52,18 @@ const saveImage = (img) => { fm.writeImage(path, img); }; -const getSavedStravaData = (filename) => { +const getSavedStravaData = async (filename) => { filename = filename ? filename : 'strava-data.json' let fm = FileManager.iCloud(); let path = fm.joinPath(fm.documentsDirectory(), filename); + try { + let downloadedFile = await fm.downloadFileFromiCloud(path); + } catch (error) { + console.error(error) + throw new Error(`If you want to run the widget from the inside the app create a file "strava-secret.json" inside Scriptable's icloud folder first.\nExpected text content: {"clientID": "", "clientSecret": "", "refreshToken": ""}`) + } let data = fm.readString(path); + console.log(`getSavedStravaData: \n${data}`); return JSON.parse(data); }; @@ -220,11 +231,13 @@ async function loadImage(imgUrl) { } async function loadActivityFromLastNDays(clientID, clientSecret, refreshToken, numberOfActivities, after) { + console.log(`TEST...\nYour ID: ${clientID}\nYour Secret: ${clientSecret}\nYour refresh token: ${refreshToken}`); try { const req = new Request(apiURL(clientID, clientSecret, refreshToken)) req.method = "POST" let response = await req.loadJSON() + console.log(response); const accessToken = response.access_token // Get data of latest activity, in this case just the ID const dataComplete = await new Request(callActivities + accessToken + `&per_page=${numberOfActivities}` + `&after=${after}`).loadJSON() @@ -242,8 +255,8 @@ async function loadActivityFromLastNDays(clientID, clientSecret, refreshToken, n } catch (e) { // If API is offline, use local data - // TODO get saved summary data as well - const offlineData = getSavedStravaData(); + const offlineData = await getSavedStravaData(); + console.log(e) console.log('using saved/offline data') return(offlineData) @@ -268,7 +281,8 @@ function getPacePer100m(moving_time_in_seconds, distance_in_meters) { } function createSummaryFromActivies(activities) { - + console.log("createSummaryFromActivies:"); + console.log(activities); // 1) CREATE SUMMARY FOR CURRENT WEEK let initialValue = 0 let total_seconds_swim = activities.filter((act) => act.type == 'Swim').reduce((act, prevActs) => act + prevActs.moving_time, initialValue) @@ -364,6 +378,9 @@ function createProgressBar(context, dayIndex, dayLabel, total_hrs_percent, sport } else if (sportType == 'Swim') { const symSwim = SFSymbol.named('humidity').image context.drawImageAtPoint(symSwim, new Point(x - 2.5, y - 25)) + } else if (sportType == 'Multi') { + const symSwim = SFSymbol.named('circlebadge.2').image + context.drawImageAtPoint(symSwim, new Point(x - 2.5, y - 25)) } else if (total_hrs_percent > 1) { //TODO: other sports... } @@ -441,21 +458,23 @@ if (widgetInput !== null) { } } else if(!config.runsInWidget){ - console.log("accessing secrets from file..."); // create that file manually inside the scriptable folder, to be able to run the script from inside the scriptable app // strava-secret.json content: // {"clientID": "", "clientSecret": "", "refreshToken": ""} - const {clientID, clientSecret, refreshToken} = getSavedStravaData('strava-secret.json'); - + const secrets = await getSavedStravaData('strava-secret.json'); + clientID = secrets.clientID; + clientSecret = secrets.clientSecret; + refreshToken = secrets.refreshToken; + console.log(`accessing secrets from file...\nYour ID: ${clientID}\nYour Secret: ${clientSecret}\nYour refresh token: ${refreshToken}`); if (!clientID || !clientSecret || !refreshToken) { - throw new Error('Invalid parameter. Expected text content: {"clientID": "", "clientSecret": "", "refreshToken": ""}') + throw new Error('Invalid text file "strava-secret.json".\nExpected text content: {"clientID": "", "clientSecret": "", "refreshToken": ""}') } } else { throw new Error("No parameters set. Please insert your parameters like this: clientID|ClientSecret|RefreshToken. Alternatively you can create a json file named 'strava-secret.json' inside the app folder") } -const apiURL = (clientID, clientSecret, refreshToken) => `https://www.strava.com/oauth/token?client_id=${clientID}&client_secret=${clientSecret}&refresh_token=${refreshToken}&grant_type=refresh_token` + const numberOfActivities = 30 //always load 30 activities and the last one complete; config.widgetFamily != 'small' ? 30 : 1; // load only 1 activity for the small widget const showLastNDays = 7 @@ -464,7 +483,7 @@ let {summary, latestActivity} = await loadActivityFromLastNDays(clientID, client console.log(latestActivity); console.log(summary); // MAIN -if(config.widgetFamily == 'small'){ +if(config.widgetFamily == 'small' || DISPLAY_SINGLE_ACTIVITY_AS_MEDIUM_WIDGET){ let widget = await createWidget(latestActivity) widget.url = "strava://feed" Script.setWidget(widget) @@ -473,7 +492,7 @@ if(config.widgetFamily == 'small'){ console.log('this runs as small widget'); } -if(config.widgetFamily == 'medium' || !config.runsInWidget){ +if((config.widgetFamily == 'medium' && !DISPLAY_SINGLE_ACTIVITY_AS_MEDIUM_WIDGET) || (!config.runsInWidget && !DISPLAY_SINGLE_ACTIVITY_AS_MEDIUM_WIDGET)){ // if(!config.runsInWidget){ const scaledSummaryData = normSummaryDataTo100Percent(summary, MAX_Y_HEIGHT); mainCreateMediumWidget(scaledSummaryData); From 477a9258c97baf1270d7995414bc399993a750bc Mon Sep 17 00:00:00 2001 From: vmarquar Date: Tue, 26 Jul 2022 13:32:45 +0200 Subject: [PATCH 7/7] fixed file not downloaded from icloud error --- strava.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/strava.js b/strava.js index 14f110c..ab8cc8f 100644 --- a/strava.js +++ b/strava.js @@ -231,13 +231,11 @@ async function loadImage(imgUrl) { } async function loadActivityFromLastNDays(clientID, clientSecret, refreshToken, numberOfActivities, after) { - console.log(`TEST...\nYour ID: ${clientID}\nYour Secret: ${clientSecret}\nYour refresh token: ${refreshToken}`); try { const req = new Request(apiURL(clientID, clientSecret, refreshToken)) req.method = "POST" let response = await req.loadJSON() - console.log(response); const accessToken = response.access_token // Get data of latest activity, in this case just the ID const dataComplete = await new Request(callActivities + accessToken + `&per_page=${numberOfActivities}` + `&after=${after}`).loadJSON() @@ -281,8 +279,6 @@ function getPacePer100m(moving_time_in_seconds, distance_in_meters) { } function createSummaryFromActivies(activities) { - console.log("createSummaryFromActivies:"); - console.log(activities); // 1) CREATE SUMMARY FOR CURRENT WEEK let initialValue = 0 let total_seconds_swim = activities.filter((act) => act.type == 'Swim').reduce((act, prevActs) => act + prevActs.moving_time, initialValue) @@ -480,8 +476,6 @@ const numberOfActivities = 30 //always load 30 activities and the last one compl const showLastNDays = 7 let {summary, latestActivity} = await loadActivityFromLastNDays(clientID, clientSecret, refreshToken, numberOfActivities, getLastMonday().MondayAsInt) -console.log(latestActivity); -console.log(summary); // MAIN if(config.widgetFamily == 'small' || DISPLAY_SINGLE_ACTIVITY_AS_MEDIUM_WIDGET){ let widget = await createWidget(latestActivity)