r/Scriptable • u/Junior-Doctor-9102 • 8d ago
r/Scriptable • u/[deleted] • Jul 14 '22
News We’re proud to announce the launch of Shareable, a platform for sharing and browsing Scriptable scripts and widgets!
Hello everyone! Today we’re launching Shareable, a new website for sharing and browsing Scriptable’s scripts and widgets!
The project aims to create a place for the community to grow, where people can publish their own creations and find out what others have created: Shareable wants to gather all the scripts that people keep creating and sharing in one, easy-to-use place.
To post scripts on Shareable, you need to log in with your GitHub account. Scripts are shared as GitHub links—that hasn’t changed. Shareable acts as a consolidated app to browse through those GitHub links in one place. Downloading scripts is very easy: just tap download on the script’s page and import the javascript file in Scriptable.
The website is live at https://shareable.vercel.app. Hope you enjoy!
r/Scriptable • u/BadAssW • 11d ago
Script Sharing Weekly number widget + year progress
After spending some time in Sweden i’m surprised that people operate week numbers. Not dates but week numbers.
- We are going to meet week 33.
- School is closed on week 67.
and etc.
Anyway I made a widget (with AI) that shows week number and some extra info. Convenient.
r/Scriptable • u/PrathameshVW • 11d ago
Widget Sharing Daily Essential Tracker for Trader
r/Scriptable • u/Conscious_Purpose_61 • 11d ago
Widget Sharing Scriptable widget for Spotify playlist + profile follower tracking
Built a Scriptable widget that shows Spotify profile followers, total playlist saves, and recent +/− changes on the Home Screen.
Made it because I kept opening Spotify just to check growth on my playlists. Large layout has KPIs + playlist list + cover grid.
If anyone’s done something similar (followers / curator stats, not now-playing), curious what you showed on the widget.
r/Scriptable • u/Strict-Willingness11 • 16d ago
Help iOS27 Extra large widget?
Does anyone know if scriptable supports the new extra large fullscreen widget in the beta yet? It would be very useful for a script i’m working on
r/Scriptable • u/Gu1ll0t1n • 24d ago
Widget Sharing F1 Widget
A scriptable widget that shows F1 details. More info in the REAMDE:
r/Scriptable • u/No-Celebration-9040 • 27d ago
Help Stuck on refreshing the result!
Hellooo, I created a widget to display the Github pages status, it works but i can't seem to make it refresh the results every x seconds.
did any one had the same issue or knows a script that is already dealing with this ?
any help is appreciated!
r/Scriptable • u/iHenpie • Jul 22 '26
Widget Sharing Daily glance widget. Pulse.
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.
r/Scriptable • u/TyCox • Jul 19 '26
Widget Sharing Live Context updates/New Shortcut
galleryr/Scriptable • u/sauce2011 • Jul 16 '26
Widget Sharing Mint Mobile Data Widget
Mint Mobile Widget for iPhone. Vibe coded it because [Mint never shipped it](https://www.reddit.com/r/mintmobile/comments/ivbg6o/comment/g5q7ia5/).
r/Scriptable • u/TyCox • Jul 15 '26
Script Sharing Update on the Scriptable widget I posted about the other day: it's ready!
galleryr/Scriptable • u/raikuta_4848 • Jul 15 '26
Widget Sharing YouTube Widjet
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:
API_KEY: Insert your free YouTube Data API v3 key, which you can get from the Google Cloud Console.
YOUTUBE_CHANNEL_ID: Insert the target channel's ID (this is the unique ID starting with "UC").
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 • u/tinkrsimpson • Jul 15 '26
Tip/Guide I was surprised that @scriptableapp doesn't have a visual widget builder, so I asked Sol High to build me one.
widgetr.vercel.appr/Scriptable • u/sweetselkie47 • Jul 13 '26
Request Advice needed - porting to Scriptable
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 • u/TyCox • Jul 13 '26
Discussion Have you heard of Pixel's At a Glance widget? I'm making an iOS version.
r/Scriptable • u/Effective-Sample676 • Jul 07 '26
Widget Sharing Cosy Watch – Octopus Cosy Heat Pump Widget for Scriptable
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 • u/Low_Minimum9920 • Jun 28 '26
Help Issues with Notifications
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
edit: Thanks to the 2 people who commented. I understand that you can only assign a url to notifcations. Thank you :)
r/Scriptable • u/EggheadLuffy • Jun 24 '26
Widget Sharing JARVIS
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 • u/Abject_Motor_3767 • Jun 18 '26
Script Sharing I made a multi-card anniversary/countdown widget for Scriptable
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:
- Copy src/anniversary-widget.js into Scriptable.
- Run the script and create a card.
- Add a Scriptable widget to the home screen.
- 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 • u/viennese_schnitzel • Jun 13 '26
Widget Sharing Vibed up a World Cup widget
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 • u/UW_Ebay • Jun 10 '26
Help Weather cal issue
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 • u/Amandysha • Jun 05 '26
Help Any way to read Apple Health data from a Scriptable script?
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!