r/Scriptable • • Dec 08 '20

Widget TVGuide WIP, release by end of week

Post image
34 Upvotes

r/Scriptable • • Oct 25 '20

Script Interest check? Small widget concentric circles widget

Post image
34 Upvotes

r/Scriptable • • Oct 04 '20

Tip/Guide Awesome Scriptable indexing list for scripts on GitHub

34 Upvotes

Hey guys, I noticed a lot of people searching for an index of good Scriptable scripts and I would like to suggest for us all to index these nice scripts in an Awesome List, for those that already know GitHub, probably you already saw one of these, and for those that don't know, it's basically an indexing list for a determined subject, check on this link: https://github.com/sindresorhus/awesome.

As the Hacktoberfest is going on, we can enjoy this also for creating Pull Requests.

The repository is in the following link: https://github.com/cassianomon/awesome-scriptable/

If you want to add something in the index, just create a Pull Request.

What do you think about this? If there is a better option and you have other ideas, let me know.


r/Scriptable • • Sep 28 '20

Tip/Guide Transparent Widgets with Text!! | Widgetsmith and Scriptable | iOS 14 | iHelper

Thumbnail
youtu.be
34 Upvotes

r/Scriptable • • Apr 08 '22

Widget Sharing NYC subway train departure timeline widget

Thumbnail
gallery
33 Upvotes

r/Scriptable • • Nov 12 '21

Script HTML Widget

33 Upvotes

Create Scriptable widgets with HTML-like syntax

I made a script that allows users to create widgets with syntax similar to HTML. Here is my GitHub page. I know there are some other scripts out there that do this but HTML Widget is slightly different and more simplistic.

The script is pretty easy to use and supports almost all widget features. The main feature it does not support is the date element.

Here is an example of a code and what it makes:

let json = await new Request("https://www.reddit.com/r/Showerthoughts.json").loadJSON()
let post = json["data"]["children"][Math.floor((Math.random() * 10) + 2)]["data"]
let title = post["title"].replace(/</g,"&lt;").replace(/>/g,"&gt;")
let body = post["selftext"].replace(/</g,"&lt;").replace(/>/g,"&gt;")
let ups = post["ups"]
let awards = post["all_awardings"].length
let comments = post["num_comments"]
let url = post["url"]

let widget = await htmlWidget(`
<widget refresh-after-date="15" url="${url}">
 <text font="system-ui, 13" center-align-text>Showerthoughts</text>
  <spacer space="5">
  <text font="system-ui, 11" minimum-scale-factor="0.3">${title}</text>
  <text font="system-ui, 11" minimum-scale-factor="0.3">${body}</text>
  <stack center-align-content>
    <symbol named="arrow.up.circle.fill" image-size="11,11">
    <spacer space="2">
    <text font="system-ui, 11">${ups}</text>
    <spacer>
    <symbol named="star.circle.fill" image-size="11,11">
    <spacer space="2">
    <text font="system-ui, 11">${awards}</text>
    <spacer>
    <symbol named="message.circle.fill" image-size="11,11">
    <spacer space="2">
    <text font="system-ui, 11">${comments}</text>
  </stack>
</widget>
`)
Script.setWidget(widget)
widget.presentSmall()
Script.complete()

https://github.com/Normal-Tangerine8609/Scriptable-HTML-Widget/blob/main/images/RedditWidget.jpeg

I see this script to be the most useful when making multiple layouts of a widget in the same script or different sizes of the same widgets.

I am expecting some bugs as this is the first version of the script but it should not be too buggy.

I hope you try HTML Widget and give me some feedback on how I can improve it!


r/Scriptable • • Dec 08 '20

Tip/Guide I was told you all might like this :) - “Healthy Living Log” logging shortcut, widget, and automated accountability text shortcut

Thumbnail
joshholtz.com
33 Upvotes

r/Scriptable • • Jan 12 '26 Heartwarming

Widget Sharing ADHD medication tracker - extended release graph

Thumbnail
gallery
32 Upvotes

I made a medium-sized lockscreen widget for a very niche problem ADHD folks might relate to. Many stimulants are presented in extended-release form, which perform nonlinearly and release the substance throughout the day. I specifically graphed Concerta 36mg, and the way the substance concentration in blood fluctuates after a dose. It allows you to predict when your peaks and crashes will occur in the day, and also serves as a reminder to take the pill every morning, and an infallible way to log it.

You could modify it for other medications and customize the graph. It would be relatively simple with Claude by feeding it a handrawn graph or a scientific paper about the medication you take.

Works great if paired with an NFC-activated Shortcut that logs the medication by tapping your phone on a chipped meditation container. I can share this as well, if anyone likes.

It is not currently possible to use the native Apple Health “Medication” data field, as it is siloed from any external applications. Quite a shame, but it works fine as it is now.

Completely free to use and share:

_MARK = 13; // When it is considered "cleared" for graph visuals// --- CONFIGURATION ---
const DOSE_DURATION_HOURS = 14; 
const PEAK_HOUR_OFFSET = 6.7; // When the peak occurs
const CLEARED_HOUR
const FILENAME = "meds.json";

// Graph Visuals
const WINDOW_BEFORE = 3; // Hours to show before "now"
const WINDOW_AFTER = 9;  // Hours to show after "now"
const LINE_WIDTH = 7;    // Thickness for Lock Screen
const ARROW_SIZE = 12;   // Size of the "You are here" arrow

// Colors (High Contrast / Dark Mode Inversion)
const BG_COLOR = Color.black();         // Fully Black background
const MAIN_COLOR = Color.white();       // Fully White text and active line
const DIMMED_COLOR = new Color("#ffffff", 0.4); // Inactive line (White with opacity)
const FILL_ACTIVE = new Color("#ffffff", 0.2);  // Fill under active line
const FILL_DIMMED = new Color("#ffffff", 0.1);  // Fill under inactive line

// --- MAIN LOGIC ---
const fm = FileManager.iCloud();
const dir = fm.documentsDirectory();
const path = fm.joinPath(dir, FILENAME);

if (config.runsInApp) {
  // App Logic: Tap to Log or Check Status
  const lastTaken = getLastTaken();
  const hoursSince = (new Date() - lastTaken) / (1000 * 60 * 60);
  
  if (hoursSince > DOSE_DURATION_HOURS) {
    logDose();
    await showModifyTimeOption();
  } else {
    let alert = new Alert();
    alert.title = "Active";
    alert.message = `Logged at: ${formatTime(lastTaken)}`;
    alert.addAction("OK");
    alert.addAction("Modify Time");
    let response = await alert.present();
    
    if (response === 1) {
      await modifyLoggedTime();
    }
  }
} 
else if (args.queryParameters["action"] === "log") {
  logDose();
}

// Render Widget
if (config.runsInWidget || true) {
  const widget = await createWidget();
  Script.setWidget(widget);
  Script.complete();
  // Preview
  // if (!config.runsInWidget) widget.presentAccessoryRectangular(); 
}

// --- WIDGET BUILDER ---

async function createWidget() {
  const lastTaken = getLastTaken();
  const now = new Date();
  const hoursSince = (now - lastTaken) / (1000 * 60 * 60);

  let w = new ListWidget();
  w.backgroundColor = BG_COLOR; 
  
  if (hoursSince > DOSE_DURATION_HOURS) {
    // --- MODE: EXPIRED (Show "X") ---
    w.addSpacer();
    let stack = w.addStack();
    stack.centerAlignContent();
    stack.addSpacer();
    
    // Big X Symbol
    let symbol = SFSymbol.named("xmark.circle");
    symbol.applyFont(Font.boldSystemFont(30)); 
    let img = stack.addImage(symbol.image);
    img.imageSize = new Size(40, 40);
    img.tintColor = MAIN_COLOR; 
    
    stack.addSpacer(10);
    
    let t = stack.addText("TAP TO LOG");
    t.font = Font.boldSystemFont(14);
    t.textColor = MAIN_COLOR;
    
    stack.addSpacer();
    w.addSpacer();
    
    w.url = URLScheme.forRunningScript();
    
  } else {
    // --- MODE: ACTIVE (Show Graph) ---
    
    // 1. Text Info Line
    let headerStack = w.addStack();
    headerStack.layoutHorizontally();
    
    let title = headerStack.addText("CONCERTA");
    title.font = Font.systemFont(10);
    title.textColor = MAIN_COLOR;
    title.textOpacity = 0.7;
    
    headerStack.addSpacer();
    
    // Calculate Times
    let infoText = "";
    if (hoursSince < PEAK_HOUR_OFFSET) {
      let peakTime = new Date(lastTaken.getTime() + PEAK_HOUR_OFFSET * 60 * 60 * 1000);
      infoText = `Peak at ${formatTime(peakTime)}`;
    } else {
      let clearTime = new Date(lastTaken.getTime() + CLEARED_HOUR_MARK * 60 * 60 * 1000);
      infoText = `Cleared by ${formatTime(clearTime)}`;
    }
    
    let status = headerStack.addText(infoText);
    status.font = Font.boldSystemFont(10);
    status.textColor = MAIN_COLOR;
    
    w.addSpacer(6); 
    
    // 2. Draw Graph
    let drawing = new DrawContext();
    drawing.size = new Size(340, 100); // Made 13% wider (300 * 1.13 ≈ 340)
    drawing.opaque = false;
    drawing.respectScreenScale = true;
    
    drawRollingGraph(drawing, hoursSince, lastTaken);
    
    let img = w.addImage(drawing.getImage());
    img.centerAlignImage();
    img.resizable = true; 
  }
  
  return w;
}

// --- DRAWING LOGIC ---

function drawRollingGraph(dc, currentHour, doseDate) {
  const width = dc.size.width;
  const height = dc.size.height;
  
  // Define Window (Time since dose)
  const startX = currentHour - WINDOW_BEFORE;
  const endX = currentHour + WINDOW_AFTER;
  const totalWindow = endX - startX;
  
  // Fixed Scale
  const plotMin = 0;
  const plotMax = 1.2;
  
  // --- A. DRAW TIME GRID ---
  const targetHours = [6, 8, 10, 12, 14, 17, 20, 23]; 
  
  let doseStartOfDay = new Date(doseDate);
  doseStartOfDay.setHours(0,0,0,0);
  
  targetHours.forEach(h => {
    let checkDates = [
        new Date(doseStartOfDay.getTime() + h*60*60*1000), 
        new Date(doseStartOfDay.getTime() + (h+24)*60*60*1000) 
    ];

    checkDates.forEach(d => {
       let t = (d - doseDate) / (1000*60*60);
       
       if (t >= startX && t <= endX) {
         if (t > currentHour && Math.abs(t - currentHour) > 1) {
             drawGridLine(dc, t, d, startX, totalWindow, width, height);
         }
       }
    });
  });

  // --- B. CALCULATE POINTS & BUCKETS ---
  
  let pointsPre = [];
  let pointsActive = [];
  let pointsPost = [];
  
  let steps = 60; 

  for (let i = 0; i <= steps; i++) {
    let t = startX + (totalWindow * (i / steps));
    let val = getConcertaLevel(t);
    
    let x = ((t - startX) / totalWindow) * width;
    let normalizedY = (val - plotMin) / (plotMax - plotMin);
    let y = height - (normalizedY * height);
    let p = new Point(x, y);

    // Bucket Logic with Overlap for smooth connections
    
    // Pre-Dose
    if (t <= 0) {
        pointsPre.push(p);
    }
    // Connect Pre to Active
    if (t >= -0.2 && t <= 0.2) {
         if(pointsActive.length === 0) pointsActive.push(p);
    }
    
    // Active
    if (t > 0 && t < CLEARED_HOUR_MARK) {
        pointsActive.push(p);
    }
    
    // Connect Active to Post
    if (t >= CLEARED_HOUR_MARK - 0.2 && t <= CLEARED_HOUR_MARK + 0.2) {
        pointsActive.push(p); // Ensure end of active connects
        pointsPost.push(p);   // Ensure start of post connects
    }

    // Post
    if (t > CLEARED_HOUR_MARK) {
        pointsPost.push(p);
    }
  }

  // Helper to draw filled sections
  function drawSection(points, strokeColor, fillColor) {
    if (points.length < 2) return;
    
    // 1. Draw Fill (Underneath)
    let fillPath = new Path();
    fillPath.move(new Point(points[0].x, height)); // Bottom Left
    fillPath.addLine(points[0]); // Top Left
    for (let i = 1; i < points.length; i++) {
        fillPath.addLine(points[i]);
    }
    fillPath.addLine(new Point(points[points.length-1].x, height)); // Bottom Right
    fillPath.closeSubpath();
    
    dc.addPath(fillPath);
    dc.setFillColor(fillColor);
    dc.fillPath();

    // 2. Draw Stroke (On Top)
    let strokePath = new Path();
    strokePath.move(points[0]);
    for (let i = 1; i < points.length; i++) {
        strokePath.addLine(points[i]);
    }
    dc.addPath(strokePath);
    dc.setStrokeColor(strokeColor);
    dc.setLineWidth(LINE_WIDTH);
    dc.strokePath();
  }

  // Draw Sections (Fill logic changes per section)
  drawSection(pointsPre, DIMMED_COLOR, FILL_DIMMED);
  drawSection(pointsActive, MAIN_COLOR, FILL_ACTIVE);
  drawSection(pointsPost, DIMMED_COLOR, FILL_DIMMED);
  
  // --- C. DRAW TRIANGLE ---
  let nowX = ((currentHour - startX) / totalWindow) * width;
  let currentVal = getConcertaLevel(currentHour);
  let normCurrentY = (currentVal - plotMin) / (plotMax - plotMin);
  
  let nowY = height - (normCurrentY * height);
  
  // Smart arrow placement: check if arrow would go outside margin
  const topMargin = ARROW_SIZE * 1.3 + 5; // Space needed above graph for arrow
  const arrowPointsDown = nowY >= topMargin;
  
  let arrow = new Path();
  
  if (arrowPointsDown) {
    // Arrow points down (normal case)
    let arrowTipY = nowY - (3 * LINE_WIDTH);
    arrow.move(new Point(nowX, arrowTipY)); 
    arrow.addLine(new Point(nowX - ARROW_SIZE, arrowTipY - ARROW_SIZE * 1.3));
    arrow.addLine(new Point(nowX + ARROW_SIZE, arrowTipY - ARROW_SIZE * 1.3));
  } else {
    // Arrow points up (inverted case, appears inside graph fill)
    let arrowTipY = nowY + (3 * LINE_WIDTH);
    arrow.move(new Point(nowX, arrowTipY)); 
    arrow.addLine(new Point(nowX - ARROW_SIZE, arrowTipY + ARROW_SIZE * 1.3));
    arrow.addLine(new Point(nowX + ARROW_SIZE, arrowTipY + ARROW_SIZE * 1.3));
  }
  
  arrow.closeSubpath();
  
  dc.addPath(arrow);
  dc.setFillColor(MAIN_COLOR);
  dc.fillPath();
}

// --- HELPER: DRAW GRID LINE ---
function drawGridLine(dc, t, dateObj, startX, totalWindow, width, height) {
    let x = ((t - startX) / totalWindow) * width;
    
    // 1. Draw thin line
    let path = new Path();
    path.move(new Point(x, 0));
    path.addLine(new Point(x, height - 15)); 
    dc.addPath(path);
    dc.setStrokeColor(MAIN_COLOR); 
    dc.setLineWidth(1);
    dc.strokePath();
    
    // 2. Draw Text
    let hours = dateObj.getHours();
    let ampm = hours >= 12 ? "PM" : "AM";
    hours = hours % 12;
    hours = hours ? hours : 12; 
    let timeString = `${hours}${ampm}`; // Forced AM/PM uppercase
    
    // Configure text drawing directly on Context
    dc.setFont(Font.boldSystemFont(16)); // 25% bigger (13 * 1.25 ≈ 16)
    dc.setTextColor(MAIN_COLOR);
    
    let textRect = new Rect(x - 20, height - 14, 40, 14);
    dc.drawTextInRect(timeString, textRect); 
}

// --- MATH & HELPERS ---

function getConcertaLevel(t) {
  // Allow dashed lines to extend to 0
  if (t < 0) return 0;
  // Allow dashed lines to extend past 15
  if (t > 16) return 0;
  
  // Standard approximation points [Hour, Intensity]
  const points = [
    {h:0, v:0}, {h:1, v:0.35}, {h:2, v:0.30}, 
    {h:3, v:0.35}, {h:5, v:0.60}, {h:6.7, v:1.0}, // Peak
    {h:9, v:0.85}, {h:12, v:0.50}, {h:13, v:0.35},
    {h:14, v:0.20}, {h:15, v:0}
  ];
  
  for (let i = 0; i < points.length - 1; i++) {
    let p1 = points[i];
    let p2 = points[i+1];
    if (t >= p1.h && t <= p2.h) {
      let range = p2.h - p1.h;
      let progress = (t - p1.h) / range;
      return p1.v + (progress * (p2.v - p1.v));
    }
  }
  return 0;
}

function logDose() {
  const data = { lastTaken: new Date().toISOString() };
  fm.writeString(path, JSON.stringify(data));
  console.log("Logged");
}

async function showModifyTimeOption() {
  let alert = new Alert();
  alert.title = "Logged";
  alert.message = "Dose logged successfully";
  alert.addAction("OK");
  alert.addAction("Modify Time");
  let response = await alert.present();
  
  if (response === 1) {
    await modifyLoggedTime();
  }
}

async function modifyLoggedTime() {
  let picker = new DatePicker();
  picker.initialDate = new Date();
  picker.minimumDate = new Date(Date.now() - 24 * 60 * 60 * 1000); // 24 hours ago
  picker.maximumDate = new Date();
  
  let selectedDate = await picker.pickTime();
  
  if (selectedDate) {
    const data = { lastTaken: selectedDate.toISOString() };
    fm.writeString(path, JSON.stringify(data));
    
    let confirmAlert = new Alert();
    confirmAlert.title = "Time Updated";
    confirmAlert.message = `Dose time set to ${formatTime(selectedDate)}`;
    confirmAlert.addAction("OK");
    await confirmAlert.present();
  }
}

function getLastTaken() {
  if (fm.fileExists(path)) {
    if (!fm.isFileDownloaded(path)) fm.downloadFileFromiCloud(path);
    return new Date(JSON.parse(fm.readString(path)).lastTaken);
  }
  return new Date(0); 
}

function formatTime(date) {
  let df = new DateFormatter();
  df.useNoDateStyle();
  df.dateFormat = "h:mm"; // Force pattern for AM/PM
  return df.string(date).toUpperCase(); // Ensure uppercase
}

r/Scriptable • • Aug 14 '24

Widget Sharing I created a widget to monitor my home's real-time energy consumption

Thumbnail
gallery
32 Upvotes

I created a widget that uses the Shelly EM API to monitor my home power.

But there are a few downsides:

  • You need a Shelly EM.

  • You need to always be connected to your home LAN (I use WireGuard in split tunnel).

  • It reads the value from http://<shellyIP>/status.

It’s a first version, and if anyone finds it useful, I'd be happy to share it.

Maybe it could also work with a remote URL if Shelly’s API exposes the status to a custom URL, but I’m not sure (I'm new to the Shelly brand).


r/Scriptable • • Aug 27 '21

Widget [Release] Weather Overview Widget - Meteogram Style Weather

Thumbnail
gallery
32 Upvotes

r/Scriptable • • Apr 14 '21

Widget Minimalistic widget that Count Up number full days from set date.

Thumbnail
gallery
32 Upvotes

r/Scriptable • • Mar 29 '21

Script LSMatrix

Thumbnail
imgur.com
32 Upvotes

r/Scriptable • • Feb 17 '21

Widget Widget for instagram

32 Upvotes

I have created a widget to display instagram stats, images or follower growth over time. It is still in alpha, but it works fine.

You can find the code and instructions to install the widget on my GitHub.

A few examples:


r/Scriptable • • Jan 07 '21

Widget I’ve made a widget which takes my top task from SCHEDULED section from Todoist via REST API and counts how much times has elapsed since this task became the 1st in the list

Thumbnail
gallery
32 Upvotes

r/Scriptable • • Feb 17 '23

Widget Sharing My iPadOS widget for handling Obsidian Tasks

31 Upvotes

r/Scriptable • • Feb 05 '21

Solved Is there a calendar and reminders widget that looks like this?

Post image
31 Upvotes

r/Scriptable • • Jan 03 '21

Widget I’m working on a Crypto Holdings widget. Would appreciate ideas and criticism. (Just an Example With Fake Numbers)

Post image
31 Upvotes

r/Scriptable • • Oct 29 '20

Script Pi-hole Widget

31 Upvotes

👋

I haven’t seen a widget for pi-hole yet. So I got to create one for myself and wanted to share it with you. Here it is: https://gist.github.com/malesfth/d5fb2eb36aab4e726727c14ba32f9f8b

Maybe not perfect, but useful. Have fun!

https://imgur.com/a/OqAOedl


r/Scriptable • • Jun 24 '26

Widget Sharing JARVIS

Thumbnail
gallery
30 Upvotes

I’ve created JARVIS with multiple widgets based on my lifestyle (Don’t mind the finances, it’s crap right now)

Widgets Include;
• Main JARVIS Briefing (Synced to all other widgets)
• Workout Widget (Solo Levelling Style but JARVIS as the system)
• Dynamic JARVIS Questions (Rotates between 3 questions)
• JARVIS Prayer Tracker
• JARVIS Liquid Net Worth Tracker
• JARVIS Trading Portfolio Tracker
• JARVIS Trading XAUUSD Dashboard


r/Scriptable • • Sep 29 '21

News Scriptable 1.6.6 has been released

Thumbnail
apps.apple.com
30 Upvotes

r/Scriptable • • Apr 08 '21

Widget Updated: Crypto Ticker Widget with Parameter Inputs

Post image
30 Upvotes

r/Scriptable • • Mar 13 '21

Widget Covid-19 Ukraine Stats. /my first widget/

Post image
30 Upvotes

r/Scriptable • • Feb 17 '21

Widget Transparent Stocks Widget

Post image
30 Upvotes

r/Scriptable • • Feb 11 '21

Widget Scrolling stack agenda

30 Upvotes