Skip to content

Instantly share code, notes, and snippets.

@laufhannes
Created July 3, 2025 12:02
Show Gist options
  • Save laufhannes/45b75eae04dc0b2f7a7971d48e0b6836 to your computer and use it in GitHub Desktop.
Save laufhannes/45b75eae04dc0b2f7a7971d48e0b6836 to your computer and use it in GitHub Desktop.
Scriptable widget for Runalyze statistics
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-blue; icon-glyph: magic;
// For API details, see https://runalyze.com/doc/api/supporter#/Current%20Statistics/api_v1statisticscurrent_get
// Read access for current statistics requires Supporter or Premium level
// You need to set your API token as widget parameter or in line 62
async function getImage(image, vImageURL) {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
if (fm.isDirectory(dir) == false) {
fm.createDirectory(dir, true)
}
let path = fm.joinPath(dir, image)
if (fm.fileExists(path)) {
await fm.downloadFileFromiCloud(path)
return fm.readImage(path)
} else {
let iconImage = await loadImage(vImageURL)
fm.writeImage(path, iconImage)
return iconImage
}
}
async function loadImage(imgUrl) {
const req = new Request(imgUrl)
return await req.loadImage()
}
function addFText2Stack(stackLeft, stackRight, stringLeft, stringRight) {
let textFont = new Font("GillSans-SemiBold", 14)
const text = stackLeft.addText(stringLeft);
const textRight = stackRight.addText(stringRight);
text.font = textFont;
textRight.font = textFont;
}
function buildWidgets(data){
let mainText = new Font("GillSans-SemiBold", 14)
let widgetLeftStack = dataStack.addStack()
widgetLeftStack.layoutVertically()
dataStack.addSpacer(20)
let widgetRightStack = dataStack.addStack()
widgetRightStack.layoutVertically()
addFText2Stack(widgetLeftStack, widgetRightStack, "Effective VO2max", data.effectiveVO2max.toLocaleString(undefined, {maximumFractionDigits: 2}));
addFText2Stack(widgetLeftStack, widgetRightStack, "Marathon Shape", `${Math.round(100 * data.marathonShape)} %`);
addFText2Stack(widgetLeftStack, widgetRightStack, "Workload Ratio A:C", (data.fatigue / data.fitness).toLocaleString(undefined, {maximumFractionDigits: 2}));
addFText2Stack(widgetLeftStack, widgetRightStack, "HRV Baseline", `${Math.round(data.hrvBaseline)} (${Math.round(data.hrvNormalRange[0])}, ${Math.round(data.hrvNormalRange[1])})`);
widget.presentMedium()
}
////////////////////////////////////////////////////
let apiToken = args.widgetParameter
if ( apiToken == null ) { apiToken = "XXX" } // Insert your own API token
let req = new Request('https://runalyze.com/api/v1/statistics/current');
req.headers = {accept: 'application/json', token: apiToken};
let response = await req.loadJSON();
let widget = new ListWidget()
let widgetMainStack = widget.addStack()
widgetMainStack.layoutVertically()
widgetMainStack.centerAlignContent()
let logoStack = widgetMainStack.addStack();
let dataStack = widgetMainStack.addStack();
let isDarkMode = Device.isUsingDarkAppearance()
if (isDarkMode) {
logoImg = await getImage("runalyze-swoosh-white.png", "https://c4.runalyze.com/build/images/runalyze-header-swoosh.bf84fe43.png") //White letters
} else {
logoImg = await getImage("runalyze-swoosh.png", "https://c4.runalyze.com/assets/images/runalyze-swoosh.png") //Black letters
}
logoStack.size = new Size(220, 40)
let theLogo = logoStack.addImage(logoImg)
logoStack.centerAlignContent()
buildWidgets(response)
Script.complete()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment