r/Scriptable 1d ago

Widget Sharing Daily glance widget. Pulse.

7 Upvotes

I have seen a couple of these on here so I decided to build a personal Scriptable widget that changes through the day: morning summary, work mode, evening wind-down and night mode. It pulls in weather, calendar/reminders, fasting, commute and Health data via Shortcuts.

It’s still a prototype, but I’d love feedback on the idea, setup flow, and what feels useful vs too much.

https://github.com/iHenpie/Pulse-Widget


r/Scriptable 4d ago

Widget Sharing Live Context updates/New Shortcut

Thumbnail gallery
8 Upvotes

r/Scriptable 7d ago

Widget Sharing Mint Mobile Data Widget

Post image
9 Upvotes

Mint Mobile Widget for iPhone. Vibe coded it because [Mint never shipped it](https://www.reddit.com/r/mintmobile/comments/ivbg6o/comment/g5q7ia5/).

https://github.com/Dr-Sauce/MintCho


r/Scriptable 8d ago

Script Sharing Update on the Scriptable widget I posted about the other day: it's ready!

Thumbnail gallery
6 Upvotes

r/Scriptable 8d ago

Widget Sharing YouTube Widjet

3 Upvotes

Description & How to Use

This is a versatile, clean iOS Scriptable widget template that displays the latest videos from your favorite YouTube channel directly on your Home Screen. It is fully responsive and automatically adjusts its layout and the number of displayed videos depending on whether you set it as a Small, Medium, Large, or Extra Large widget.

Key Features

The widget supports two main modes of operation. By default, it displays the channel’s most recently uploaded videos (Normal Mode). However, you can also set it to "Random Mode," which uses a fast, two-step API request to shuffle and showcase random videos from the channel's entire history. Tapping on any video inside the widget will open it directly in YouTube, and tapping the header will take you to the channel page.

Setup Guide

First, copy the template code and paste it into a new script within the Scriptable app. You will need to customize the configuration section at the very top of the script with three details:

  1. API_KEY: Insert your free YouTube Data API v3 key, which you can get from the Google Cloud Console.

  2. YOUTUBE_CHANNEL_ID: Insert the target channel's ID (this is the unique ID starting with "UC").

  3. YOUTUBE_CHANNEL_URL: Insert the channel's custom handle URL (e.g., https://youtube.com/@username).

Advanced Customization (Widget Parameters)

You can further customize the widget's behavior by typing a value into the "Parameter" field in the iOS widget settings:

Leave it blank to show the latest videos (Page 1).

Type a number (like ⁠2⁠ or ⁠3⁠) to paginate and show older videos from previous pages.

Type ⁠random⁠ or ⁠rnd⁠ to trigger the Random Mode and enjoy a shuffled selection of videos every time the widget refreshes.

Code:

// =========================================================================
// [Required Configuration] Your YouTube API Key and Channel Information
// =========================================================================
// Please input your Google Cloud YouTube Data API v3 key here.
// NEVER distribute this script with your actual API key written inside.
const API_KEY = "YOUR_API_KEY_HERE"; 

// Your YouTube Channel ID (Starts with "UC...")
const YOUTUBE_CHANNEL_ID = "YOUR_CHANNEL_ID_HERE"; 
// Your YouTube Custom URL or Handle (e.g., "https://youtube.com/@username")
const YOUTUBE_CHANNEL_URL = "https://youtube.com/@YOUR_CHANNEL_HANDLE";

// Automatically generate the Uploads Playlist ID by replacing the 2nd character 'C' with 'U'
const UPLOADS_PLAYLIST_ID = YOUTUBE_CHANNEL_ID.replace(/^UC/, "UU");

// Channel Icon fetch URL (Using unavatar.io with a fallback default avatar)
const CHANNEL_ICON_URL = `https://unavatar.io/youtube/${YOUTUBE_CHANNEL_ID}?fallback=https://www.youtube.com/s/desktop/28169123/img/avatar_ghost.png`;

// =========================================================================
// ★ Parameter Processing for Widget (Page Number or Random Mode)
// =========================================================================
let pageNum = 1;
let isRandomMode = false;
let widgetParam = args.widgetParameter;

if (widgetParam) {
  let paramStr = widgetParam.trim().toLowerCase();
  if (paramStr === "rnd" || paramStr === "random") {
    isRandomMode = true;
    console.log("🎲 [Mode] Random video retrieval mode has been activated.");
  } else {
    let parsed = parseInt(paramStr, 10);
    if (!isNaN(parsed) && parsed > 0) {
      pageNum = parsed;
    }
  }
}

// =========================================================================
// Widget Size and Item Limit Settings
// =========================================================================
let size = config.widgetFamily;
if (config.runsInApp) {
  size = "extraLarge"; 
}

// Set maximum items based on widget size
let maxItems = 2;
if (size === "extraLarge") {
  maxItems = 20;
} else if (size === "large") {
  maxItems = 6;
} else if (size === "medium") {
  maxItems = 4; 
} else if (size === "small") {
  maxItems = 2; 
}

// =========================================================================
// YouTube Data API (v3) Data Fetching
// =========================================================================
let entryTitles = [];
let videoIds = [];
let thumbUrls = [];
let channelName = "YouTube Channel";
let isApiSuccess = false;

let pageToken = "";
let apiErrorOccurred = false;

if (isRandomMode) {
  // -----------------------------------------------------------------------
  // 🎲 Random Mode: Fast 2-step API Request
  // -----------------------------------------------------------------------
  try {
    console.log("🎲 [1/2] Fetching the first page to determine total video count...");
    let firstUrl = `https://www.googleapis.com/youtube/v3/playlistItems?key=${API_KEY}&playlistId=${UPLOADS_PLAYLIST_ID}&part=snippet&maxResults=50`;
    let req1 = new Request(firstUrl);
    let res1 = await req1.loadJSON();

    if (res1 && res1.items && res1.items.length > 0) {
      channelName = res1.items[0].snippet.channelTitle || "YouTube Channel";
      let totalResults = res1.pageInfo.totalResults;
      console.log(`📊 Total videos in channel: ${totalResults}`);

      if (totalResults > maxItems) {
        // Calculate a random start index
        let maxStartIndex = totalResults - maxItems;
        let randIndex = Math.floor(Math.random() * maxStartIndex);
        console.log(`🎯 Target random video index selected: around ${randIndex}`);

        // Calculate target page and offset within that page (50 items per page)
        let targetPage = Math.floor(randIndex / 50);
        let itemOffsetInPage = randIndex % 50;

        // Traverse pages using tokens
        let currentToken = "";
        let currentRes = res1;
        for (let step = 0; step < targetPage; step++) {
          currentToken = currentRes.nextPageToken;
          if (!currentToken) break;

          let pageUrl = `https://www.googleapis.com/youtube/v3/playlistItems?key=${API_KEY}&playlistId=${UPLOADS_PLAYLIST_ID}&part=snippet&maxResults=50&pageToken=${currentToken}`;
          let reqStep = new Request(pageUrl);
          currentRes = await reqStep.loadJSON();
        }

        // Extract the specific range of items
        let targetItems = currentRes.items.slice(itemOffsetInPage, itemOffsetInPage + maxItems);

        // Handle page boundary item shortfall
        if (targetItems.length < maxItems && currentRes.nextPageToken) {
          let nextPageUrl = `https://www.googleapis.com/youtube/v3/playlistItems?key=${API_KEY}&playlistId=${UPLOADS_PLAYLIST_ID}&part=snippet&maxResults=50&pageToken=${currentRes.nextPageToken}`;
          let reqNext = new Request(nextPageUrl);
          let nextRes = await reqNext.loadJSON();
          if (nextRes && nextRes.items) {
            targetItems = targetItems.concat(nextRes.items.slice(0, maxItems - targetItems.length));
          }
        }

        if (targetItems.length > 0) {
          isApiSuccess = true;
          for (let item of targetItems) {
            entryTitles.push(item.snippet.title);
            if (item.snippet.resourceId && item.snippet.resourceId.videoId) {
              videoIds.push(item.snippet.resourceId.videoId);
            } else {
              videoIds.push("");
            }
            let thumb = item.snippet.thumbnails.medium || item.snippet.thumbnails.default;
            thumbUrls.push(thumb ? thumb.url : "");
          }
        }
      } else {
        // Use first page data directly if total count is less than widget capacity
        isApiSuccess = true;
        for (let item of res1.items) {
          entryTitles.push(item.snippet.title);
          videoIds.push(item.snippet.resourceId.videoId || "");
          let thumb = item.snippet.thumbnails.medium || item.snippet.thumbnails.default;
          thumbUrls.push(thumb ? thumb.url : "");
        }
      }
    } else {
      apiErrorOccurred = true;
    }
  } catch(e) {
    console.error("❌ Random Mode Fetch Failed: " + e.message);
    apiErrorOccurred = true;
  }
} else {
  // -----------------------------------------------------------------------
  // 📄 Normal Mode: Fetch videos for the specified page number
  // -----------------------------------------------------------------------
  for (let p = 1; p <= pageNum; p++) {
    let tokenParam = pageToken ? `&pageToken=${pageToken}` : "";
    let apiUrl = `https://www.googleapis.com/youtube/v3/playlistItems?key=${API_KEY}&playlistId=${UPLOADS_PLAYLIST_ID}&part=snippet&maxResults=${maxItems}${tokenParam}`;

    try {
      let apiReq = new Request(apiUrl);
      let apiResponse = await apiReq.loadJSON();

      if (apiResponse && apiResponse.items && apiResponse.items.length > 0) {
        if (p === pageNum) {
          isApiSuccess = true;
          channelName = apiResponse.items[0].snippet.channelTitle || "YouTube Channel";

          for (let item of apiResponse.items) {
            entryTitles.push(item.snippet.title);
            if (item.snippet.resourceId && item.snippet.resourceId.videoId) {
              videoIds.push(item.snippet.resourceId.videoId);
            } else {
              videoIds.push("");
            }
            let thumb = item.snippet.thumbnails.medium || item.snippet.thumbnails.default;
            thumbUrls.push(thumb ? thumb.url : "");
          }
        }
        pageToken = apiResponse.nextPageToken;
        if (!pageToken) break; 
      } else {
        apiErrorOccurred = true;
        break;
      }
    } catch (e) {
      console.error("❌ Normal Mode Fetch Failed: " + e.message);
      apiErrorOccurred = true;
      break;
    }
  }
}

function formatTitle(text, charsPerLine, maxLines) {
  let lines = [];
  for (let i = 0; i < text.length; i += charsPerLine) {
    lines.push(text.substr(i, charsPerLine));
  }
  return lines.slice(0, maxLines).join("\n");
}

// =========================================================================
// Tap Action Handling
// =========================================================================
if (config.runsInApp && args.queryParameters.url) {
  Safari.open(args.queryParameters.url);
  Script.complete();
} else {
  // =========================================================================
  // UI Construction
  // =========================================================================
  let widget = new ListWidget();
  let gradient = new LinearGradient();
  gradient.colors = [new Color("#111111"), new Color("#222222")];
  gradient.locations = [0.0, 1.0];
  widget.backgroundGradient = gradient;

  if (size === "large") {
    widget.setPadding(10, 4, 10, 4);
  } else {
    widget.setPadding(8, 4, 8, 4);
  }

  // --- Header Section ---
  let headerStack = widget.addStack();
  headerStack.layoutHorizontally();
  headerStack.url = YOUTUBE_CHANNEL_URL; 
  headerStack.setPadding(0, 4, 0, 0);

  try {
    let iconReq = new Request(CHANNEL_ICON_URL);
    let iconImg = await iconReq.loadImage();
    let iconElem = headerStack.addImage(iconImg);
    iconElem.imageSize = new Size(24, 24);
    iconElem.cornerRadius = 12;
    headerStack.addSpacer(4);
  } catch(e) {
    headerStack.addText("📺 ");
  }

  // Determine Title Text based on settings
  let displayTitle = `${channelName}`;
  if (isRandomMode) {
    displayTitle = `🎲 ${channelName} (Shuffle)`;
  } else if (pageNum > 1) {
    let startNum = ((pageNum - 1) * maxItems) + 1;
    let endNum = pageNum * maxItems;
    displayTitle = `${channelName} (#${startNum}-${endNum})`;
  } else {
    displayTitle = `${channelName} (Latest)`;
  }

  let titleText = headerStack.addText(displayTitle);
  titleText.textColor = new Color("#ff4500");
  titleText.font = Font.boldSystemFont(15);

  widget.addSpacer((size === "large") ? 10 : 6);

  // --- Video List Section ---
  if (!isApiSuccess || entryTitles.length === 0) {
    let errorText = widget.addText("⚠ YouTube API Error");
    errorText.textColor = new Color("#aaaaaa");
    errorText.font = Font.systemFont(11);
  } else {
    let currentCount = Math.min(entryTitles.length, maxItems);

    if (size === "extraLarge" || size === "medium") {
      let gridStack = widget.addStack();
      gridStack.layoutVertically();

      let rows = (size === "extraLarge") ? 5 : 2;
      let cols = (size === "extraLarge") ? 4 : 2; 
      let cellWidth = (size === "extraLarge") ? 170 : 155; 
      let cellHeight = (size === "extraLarge") ? 50 : 45; 

      for (let r = 0; r < rows; r++) {
        let rowGrid = gridStack.addStack();
        rowGrid.layoutHorizontally();

        for (let c = 0; c < cols; c++) {
          let index = r * cols + c;

          if (index >= currentCount) {
            let emptyCell = rowGrid.addStack();
            emptyCell.size = new Size(cellWidth, cellHeight);
            if (c < cols - 1) rowGrid.addSpacer(4);
            continue;
          }

          let videoTitle = entryTitles[index];
          let videoId = videoIds[index];
          let videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
          let thumbUrl = thumbUrls[index];

          let cellStack = rowGrid.addStack();
          cellStack.layoutHorizontally();
          cellStack.url = videoUrl;
          cellStack.size = new Size(cellWidth, cellHeight); 
          cellStack.centerAlignContent(); 

          if (thumbUrl) {
            try {
              let imgReq = new Request(thumbUrl);
              let img = await imgReq.loadImage();
              let imgElem = cellStack.addImage(img);
              imgElem.imageSize = (size === "extraLarge") ? new Size(64, 36) : new Size(48, 27); 
              imgElem.cornerRadius = 4;
              cellStack.addSpacer(6);
            } catch(e) {
              let dot = cellStack.addText("• ");
              dot.textColor = new Color("#888888");
            }
          }

          let titleElem = cellStack.addText(videoTitle);
          titleElem.textColor = new Color("#ffffff");
          titleElem.font = Font.systemFont((size === "extraLarge") ? 12.5 : 10); 
          titleElem.lineLimit = 2; 

          if (c < cols - 1) rowGrid.addSpacer(4);
        }
        if (r < rows - 1) gridStack.addSpacer(4);
      }
    } else {
      for (let i = 0; i < currentCount; i++) {
        let videoTitle = entryTitles[i];
        let videoId = videoIds[i];
        let videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
        let thumbUrl = thumbUrls[i];

        let rowStack = widget.addStack();
        rowStack.layoutHorizontally();
        rowStack.url = videoUrl; 

        if (size === "large") {
          rowStack.setPadding(4, 0, 4, 0);
        } else {
          rowStack.setPadding(2, 0, 2, 0);
        }

        if (thumbUrl) {
          try {
            let imgReq = new Request(thumbUrl);
            let img = await imgReq.loadImage();
            let imgElem = rowStack.addImage(img);
            imgElem.imageSize = (size === "large") ? new Size(56, 31) : new Size(50, 28); 
            imgElem.cornerRadius = 4;
            rowStack.addSpacer((size === "large") ? 8 : 6);
          } catch(e) {
            rowStack.addText("• ");
          }
        }

        let titleElem = rowStack.addText(videoTitle);
        titleElem.textColor = new Color("#ffffff");
        titleElem.font = Font.systemFont((size === "large") ? 12 : 11);  
        titleElem.lineLimit = 2;

        if (i < currentCount - 1) {
          widget.addSpacer((size === "large") ? 4 : 2);
        }
      }
    }
  }

  Script.setWidget(widget);
  Script.complete();
}

r/Scriptable 8d ago

Tip/Guide I was surprised that @scriptableapp doesn't have a visual widget builder, so I asked Sol High to build me one.

Thumbnail widgetr.vercel.app
12 Upvotes

r/Scriptable 10d ago

Request Advice needed - porting to Scriptable

2 Upvotes

TL/dr
advice needed re: porting API pre and post-processors to Scriptable iOS app

---

I'm a newbie coder using Apple Shortcuts as a runner for some API calls and post-processing.

On my Mac, I'm using the shell function, but that's not supported on iOS so I'm using Scriptable as a workaround.

Trouble is that copypastaing my existing Javascript directly into Scriptable doesn't work and I'm not sure what to "wrap" it in.

Does anyone have advice for how to approach this?

Many thanks in advance!


r/Scriptable 10d ago

Discussion Have you heard of Pixel's At a Glance widget? I'm making an iOS version.

Thumbnail
5 Upvotes

r/Scriptable 16d ago

Widget Sharing Cosy Watch – Octopus Cosy Heat Pump Widget for Scriptable

2 Upvotes

I wanted a quick way to see key information from my Octopus Cosy heat pump directly on my iPhone Home Screen without opening the Octopus app, so I put together a Scriptable widget and thought I'd share it with the community.

Cosy Watch pulls data from the Octopus API and displays:

  • Heat pump status
  • Hot water temperature
  • COP
  • Heat output
  • Power usage
  • Outside temperature
  • Cosy Pod temperatures
  • Average room temperature
  • Temperature range across pods
  • Average humidity
  • Today's energy use
  • Yesterday's energy use
  • Fault highlighting and fault codes (where available)

The project includes installation instructions and screenshots:

More info
https://tstomsmith-arch.github.io/cosy-watch-octopus-heat-pump-widget/

GitHub repository
https://github.com/tstomsmith-arch/cosy-watch-octopus-heat-pump-widget

This was built as a personal project and is not affiliated with Octopus Energy.

I'm still improving it, so I'd welcome any feedback, suggestions, or ideas from other Scriptable users.


r/Scriptable 25d ago

Help Issues with Notifications

1 Upvotes

I've recently gotten into Scriptable because it's rather the only way to actually do funky stuff on your iphone but that aside how do I make notifications actually do something?

I was thinking of using the .addAction but it doesn't really let me assign any function to it


r/Scriptable 29d ago

Widget Sharing JARVIS

Thumbnail
gallery
29 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 Jun 18 '26

Script Sharing I made a multi-card anniversary/countdown widget for Scriptable

6 Upvotes

I made a Scriptable widget called Anniversary Cards:

https://github.com/chenxq-297/scriptable-widgets

It supports multiple anniversary/countdown cards in one script. Each saved card has an id like wedding or birthday, and each iOS home screen widget instance uses the Scriptable Widget Parameter to decide which card to show.

Features:

  • create multiple cards from inside Scriptable
  • edit, copy id, and delete from one card menu
  • automatically copy the card id after create/edit
  • per-card background choices
  • photo backgrounds are copied locally per card
  • local-only storage through Scriptable Keychain and Scriptable documents

Install:

  1. Copy src/anniversary-widget.js into Scriptable.
  2. Run the script and create a card.
  3. Add a Scriptable widget to the home screen.
  4. Edit the widget and paste the card id into Parameter.

Stable raw script: https://raw.githubusercontent.com/chenxq-297/scriptable-widgets/v0.1.0/src/anniversary-widget.js

Small limitation: iOS still requires adding/editing the home screen widget manually. Scriptable scripts cannot directly add widgets to the home screen, and this is not a Dynamic Island / Live Activity widget.


r/Scriptable Jun 17 '26

Script Sharing 我做了一个世界杯小组件

Thumbnail
gallery
21 Upvotes

r/Scriptable Jun 13 '26

Widget Sharing Vibed up a World Cup widget

Post image
33 Upvotes

lotta hard coding and wonky stuff but gets the job done. this was my first time using scriptable, it’s super cool! lmk if folks want the js happy to share.

Edit - made some improvements to the UI/UX, GH link: https://github.com/tonesf/fifa-world-cup-widget


r/Scriptable Jun 10 '26

Help Weather cal issue

Post image
1 Upvotes

Is anyone else using weather cal still?

My widget stopped working today all of a sudden, I assume due to the latest iOS update. (26.5.1)

Any suggestions on how to fix? Maybe just create a new widget?


r/Scriptable Jun 05 '26

Help Any way to read Apple Health data from a Scriptable script?

2 Upvotes

Hi! Is there any way to read Apple Health data (specifically sleep analysis with phases like REM, deep, core) from a Scriptable script?

I tried new HealthKit() and Health.createSource() but both return ReferenceError.

Is HealthKit supported in Scriptable, and if so, what is the correct syntax? I’m on iOS 26 with the latest version of Scriptable.

Thanks!


r/Scriptable Jun 02 '26

Script Sharing Youtube widget script

8 Upvotes

This is a Custom YouTube Latest Videos Widget built for the iOS app Scriptable.

It connects directly to a YouTube channel's public RSS feed to fetch and display the most recently uploaded videos in a sleek, customized list on your iOS Home Screen. It bypasses heavy API restrictions, loads incredibly fast, and features auto-wrapping text tailored dynamically to Small, Medium, and Large widget sizes.

If you want to change the widget to track a different YouTuber in the future, just modify the very top section of the code:

const YOUTUBE_CHANNEL_ID = "YOUR_NEW_CHANNEL_ID_HERE";

// ==========================================
// [Settings] Channel ID & URL
// ==========================================
const YOUTUBE_CHANNEL_ID = "YOUR_CHANNEL_ID_HERE"; 
const YOUTUBE_CHANNEL_URL = `https://youtube.com/channel/${YOUTUBE_CHANNEL_ID}`;

// High-quality avatar endpoint
const CHANNEL_ICON_URL = `https://unavatar.io/youtube/${YOUTUBE_CHANNEL_ID}?fallback=https://www.youtube.com/s/desktop/28169123/img/avatar_ghost.png`;

// ==========================================
// 1. Fetch YouTube RSS Feed & Parse XML
// ==========================================
const rssUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${YOUTUBE_CHANNEL_ID}`;
let rssReq = new Request(rssUrl);
let xmlString = "";

try {
  xmlString = await rssReq.loadString();
} catch(e) {
  xmlString = ""; 
}

function getTags(xml, tagName) {
  if (!xml) return [];
  let expr = new RegExp(`<${tagName}>([^<]*)</${tagName}>`, "g");
  let matches = [];
  let match;
  while ((match = expr.exec(xml)) !== null) {
    matches.push(match[1]);
  }
  return matches;
}

function getThumbnails(xml) {
  if (!xml) return [];
  let expr = /<media:thumbnail[^>]+url="([^"]+)"/g;
  let matches = [];
  let match;
  while ((match = expr.exec(xml)) !== null) {
    matches.push(match[1]);
  }
  return matches;
}

// Function to wrap text at specific character length and limit lines
function formatTitle(text, charsPerLine, maxLines) {
  if (!text) return "";
  let lines = [];
  for (let i = 0; i < text.length; i += charsPerLine) {
    lines.push(text.substr(i, charsPerLine));
  }
  return lines.slice(0, maxLines).join("\n");
}

let entryTitles = getTags(xmlString, "title");
let videoIds = getTags(xmlString, "yt:videoId");
let authorNames = getTags(xmlString, "name");
let thumbUrls = getThumbnails(xmlString);

let channelName = authorNames[0] || "YouTube Channel";
if (entryTitles[0] === channelName) {
  entryTitles.shift();
}

// ==========================================
// 2. Handle Tap Actions (Open Safari)
// ==========================================
if (config.runsInApp && args.queryParameters.url) {
  Safari.open(args.queryParameters.url);
  Script.complete();
} else {
  // ==========================================
  // 3. Determine Widget Size & Build UI
  // ==========================================
  let size = config.widgetFamily;

  if (config.runsInApp) {
    size = "large";
  }

  // [Display Count Logic based on Widget Size]
  let maxItems = 2;
  if (size === "large") {
    maxItems = 6; // 6 items for Large
  } else if (size === "small") {
    maxItems = 5; // 5 items for Small
  } else {
    maxItems = 2; // 2 items for Medium
  }

  let widget = new ListWidget();

  let gradient = new LinearGradient();
  gradient.colors = [new Color("#1a1a1a"), new Color("#111111")];
  gradient.locations = [0.0, 1.0];
  widget.backgroundGradient = gradient;

  if (size === "small") {
    widget.setPadding(2, 6, 2, 6);
  } else {
    widget.setPadding(14, 14, 14, 14);
  }

  // --- Header Section (Icon & Channel Name) ---
  let headerStack = widget.addStack();
  headerStack.layoutHorizontally();
  headerStack.url = YOUTUBE_CHANNEL_URL; 

  try {
    let iconReq = new Request(CHANNEL_ICON_URL);
    let iconImg = await iconReq.loadImage();
    let iconElem = headerStack.addImage(iconImg);
    let iconSize = (size === "small") ? 12 : 24; 
    iconElem.imageSize = new Size(iconSize, iconSize);
    iconElem.cornerRadius = iconSize / 2;
    headerStack.addSpacer(4);
  } catch(e) {
    let fallbackEmoji = headerStack.addText("📺 ");
    fallbackEmoji.font = Font.systemFont(size === "small" ? 9 : 14);
  }

  // Header text configuration
  let titleText = headerStack.addText(size === "small" ? channelName : `${channelName}`);
  titleText.textColor = new Color("#ff0000"); // YouTube Red
  titleText.font = Font.boldSystemFont(size === "small" ? 9 : 15);

  widget.addSpacer(size === "small" ? 1 : 10);

  // --- Video List Section ---
  if (entryTitles.length === 0) {
    let errorText = widget.addText("⚠ No Videos Found");
    errorText.textColor = new Color("#aaaaaa");
    errorText.font = Font.systemFont(11);
  } else {
    let currentCount = Math.min(entryTitles.length, maxItems);
    for (let i = 0; i < currentCount; i++) {
      let videoTitle = entryTitles[i];
      let videoId = videoIds[i];
      let videoUrl = `https://www.youtube.com/watch?v=${videoId}`;
      let thumbUrl = thumbUrls[i];

      let rowStack = widget.addStack();
      rowStack.layoutHorizontally();
      rowStack.url = videoUrl; 

      rowStack.setPadding(size === "small" ? 0.2 : 4, 0, size === "small" ? 0.2 : 4, 0);

      // Thumbnail logic
      if (size !== "small" && thumbUrl) {
        try {
          let imgReq = new Request(thumbUrl);
          let img = await imgReq.loadImage();
          let imgElem = rowStack.addImage(img);
          let thumbW = (size === "large") ? 64 : 56;
          let thumbH = (size === "large") ? 36 : 31;
          imgElem.imageSize = new Size(thumbW, thumbH); 
          imgElem.cornerRadius = 4;
          rowStack.addSpacer(8);
        } catch(e) {
          let dot = rowStack.addText("• ");
          dot.textColor = new Color("#888888");
        }
      } else if (size === "small") {
        let dot = rowStack.addText("• ");
        dot.textColor = new Color("#888888");
        dot.font = Font.systemFont(8);
      }

      // Force wrap based on size limits
      if (size === "large") {
        videoTitle = formatTitle(videoTitle, 20, 3);
      } else if (size === "small") {
        videoTitle = formatTitle(videoTitle, 15, 2);
      }

      let titleElem = rowStack.addText(videoTitle);
      titleElem.textColor = new Color("#ffffff");

      if (size === "small") {
        titleElem.font = Font.systemFont(8.5); 
      } else if (size === "large") {
        titleElem.font = Font.systemFont(11);  
      } else {
        titleElem.font = Font.systemFont(12);  
      }

      if (size === "large") {
        titleElem.lineLimit = 3;
      } else {
        titleElem.lineLimit = 2;
      }

      if (i < currentCount - 1) {
        widget.addSpacer(size === "small" ? 0.2 : 4);
      }
    }
  }

  // ==========================================
  // 4. Finalize Script & Present Widget
  // ==========================================
  Script.setWidget(widget);

  if (config.runsInApp) {
    widget.presentLarge(); 
  }

  Script.complete();
}

r/Scriptable May 31 '26

Help World cup wallpaper

7 Upvotes

Hi guys, I was looking for a way to update m’y wallpaper daily with the World cup schedule and discovered scriptable. The problem is I don’t understand how to set up my automatisation for it 🫠 could anyone Please help me 😭


r/Scriptable May 27 '26

Script Sharing MLB script

Thumbnail
gallery
8 Upvotes

I’m working hard on it, and it’ll be finished soon!


r/Scriptable May 15 '26

Script Sharing SORONICE HUB

1 Upvotes

Pour ceux qui veulent avoir plus d'informations sur mon script, tout simplement, et ben il faudra, euh, il faudra aller sur un site que je vais créer spécialement pour avoir plus d'informations et qui sera tout complet. Et vous pourrez rechercher des informations, tout ça là-dessus. Et je vais le faire spécialement pour toutes les demandes, tout ça, comme ça, pour les plaintes aussi, pour ceux qui se plaignent des, des, des, des ralentissements, tout ça, que mes scripts ont des problèmes, tout ça. Vous pourrez envoyer des messages d'erreur ou des captures d'écran avec des messages, tout ça. Du coup, faire ce genre de système là pour que-- pour voir les problèmes que vous avez, du code que vos notes discord pour l'afficher, pour que je sache c'est qui, pour vous dire après les phrases suivantes. Comme ça, ça va vous aider et ça va vous simplifier la vie aussi, si je puisse dire. En tout voilà, quoi. Ce genre de truc-là, c'est pas mal, c'est pas mal quoi. C'est pas mal, voilà. En tout, c'est pas encore terminé, mais pas comme si je vais dire abandonné. C'est toujours là quoi. Et aussi, pour spécifier le nom quand vous avez vu un titre, mais ça va passer au script. C'est le nom de mon script et je compte seulement vous donner le script, comme ça, vous pouvez l'utiliser pour tester sur tous les jeux : 99 nuits, Block Speed, Muscles Légendes et encore d'autres jeux. Les autres jeux que je ne peux pas citer. Arrivo et aussi l'Empire du Bois 2. Voilà, je citais tous les noms. Voilà:loadstring(game:HttpGet('https://raw.githubusercontent.com/Audinay/UFIL/refs/heads/main/LICENSE/Biblioth%C3%A8que%20de%20SCRIPTS/Ouvrage/SORONICE%20HUB.lua'))()())


r/Scriptable May 12 '26

Widget Sharing Scriptable habits and apple reminders

12 Upvotes

GIT: https://github.com/jabusbb/scriptable-habits-and-reminders

Minimalist widgets for Scriptable that integrate with Apple Reminders to display habits and daily tasks directly on your iPhone home screen. The widgets allow you to quickly track habits

when your throat hurts and your head is working, play with ai.


r/Scriptable May 07 '26

Widget Sharing I built a Formula 1 event tracker widget

Thumbnail
gallery
27 Upvotes

Hey all, for the past few months I've been working on a simple Formula 1 event tracker widget I felt should have been a feature in the F1 TV app or Apple TV now that Apple owns broadcasting rights in the US. I created this widget with one goal in mind: have the most relevant weekend start times available to me at a glance from the home screen, without having to search for and convert dates/times. Now that I have developed it to a point where I like it, I wanted to share my progress on the project.

What it does:

• Shows the track layout outline for the current race weekend

• Displays session status with full timezone support so you always see your own local start times

• Live updates on the current session state: upcoming, live, or complete

• Shows the next upcoming session's name at a glance

Post-session leaderboard - shows off the top 5 finishers for Race, Qualifying, Sprint, and Sprint Qualifying, mimicking the F1 TV broadcast layout with dynamic background color changes based on the winning team

Basically everything I felt I wanted to know without needing to open the F1 TV app or look for the info on the main site.

Current state:

Small widget only right now. I plan to add more qol improvements as time goes on. Medium widget support is still in development, with the intention to show an extended weekend schedule alongside the current session. Large widget is in early design stages.

How to install:

Grab the latest release from the GitHub link, move the javascript file into your iCloud Drive/Scriptable folder, add the widget to your home screen, and select the script. Full step-by-step in the README for those unfamiliar.

GitHub: https://github.com/amart251/F1-events-tracker-widget

Happy to answer questions or hear feedback. I've been testing and developing this solo on and off for the past 8 months now, so if you spot anything weird with timezone handling across different locales or any bugs you encounter, comment down or create an issue/pr on the project page.


r/Scriptable Apr 30 '26

Help Made a Scriptable app launcher! Need feedback

Post image
26 Upvotes

Is there any way to open apps directly without triggering the scriptable app? If I open an app it 1st opens scriptable and then redirect it to the selected app! That's the only downside!

Would you guys be interested to use this? Plz Lemme know!

EDIT : check out the script here


r/Scriptable Apr 24 '26

Script Sharing I made my first Scriptable App, Thanks for any suggestions

Post image
2 Upvotes

New to this so if you have any suggests let me know.

Also how do I share this ? I see the app has a library with cool icons.

my app

scriptable:///run/Stock%20Sector%20Indicator%20

What it shows for all 11 SPDR Sector ETFs:

• Gauge zone (Overbought / Hold / Buy / Extreme Buy) — color coded

• Technical Signal (Trending Up / Moderate / Trending Down)

• Fair Value % — color coded from deep value (green) to expensive (red)Paste the script (link in comments)

. Add as a Large widget OR run full screen

Thanks for your input


r/Scriptable Apr 20 '26

Solved Can padding be removed from lock screen widgets?

Post image
7 Upvotes

Sorry if this is a repeat question, I searched but couldn’t find an answer.

The widget on the right (Recently Played) is the Scriptable widget. I was sort of hoping it could be left more left aligned like the other one. I think it is though? And there’s just extra padding? I tried setting padding to 0, idk what I’m missing. Partial script below, I’ve removed the part with the API requests. I imagine that’s not important to this issue. It’s in a weird state from all the attempts I made. Making this post from my phone, if there’s a nicer way to post code, I can’t find it (,:

const widget = new ListWidget()

const wrapper = widget.addStack()

wrapper.layoutVertically()

const headerStack = wrapper.addStack()

const game = wrapper.addStack()

widget.addAccessoryWidgetBackground = true

widget.setPadding(0, 0, 0, 0)

const header = headerStack.addText("Recently Played")

header.leftAlignText()

game.centerAlignContent()

const { icon, name } = await getGameData()

game.addImage(icon)

game.addText(name).leftAlignText()

if (config.runsInAccessoryWidget) {

Script.setWidget(widget)

} else {

widget.presentAccessoryRectangular()

}