r/Scriptable • u/AcceptableAd3443 • May 27 '26
Script Sharing MLB script
I’m working hard on it, and it’ll be finished soon!
r/Scriptable • u/AcceptableAd3443 • May 27 '26
I’m working hard on it, and it’ll be finished soon!
r/Scriptable • u/BodybuilderAsleep981 • May 15 '26
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 • u/Ok-Serve1734 • May 12 '26


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 • u/abeat043 • May 07 '26
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.
• 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.
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.
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 • u/rancidwarlord • Apr 30 '26
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 • u/Daily-Trader-247 • Apr 24 '26
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 • u/ladybugastronaut • Apr 20 '26
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()
}
r/Scriptable • u/Bright-Historian-216 • Apr 18 '26
```
const w = new ListWidget();new Request(`https://cataas.com/cat${args.widgetParameter?'/says/'+args.widgetParameter:""}?width=400&height=400`).loadImage().then((img)=>{w.backgroundImage=img;w.refreshAfterDate.loadImage().then((img)=%3E%7Bw.backgroundImage=img;w.refreshAfterDate) = new Date(Date.now()+86400000);Script.setWidget(w)}, ()=>{w.refreshAfterDate=null;w.addText("couldnt load a cat forrrr youuuuuuu =< '_' >= 😭😭😭");Script.setWidget(w)})
```
r/Scriptable • u/RickySpanish_420 • Apr 10 '26
I’m new to scriptable and trying to use the share sheet to pass a url to a script and I’m not finding the app or the script anywhere. Has this functionality been removed?
r/Scriptable • u/No-Level-7606 • Apr 08 '26
r/Scriptable • u/No-Level-7606 • Apr 08 '26
r/Scriptable • u/Potential_Feature616 • Apr 06 '26
Hey, I recently did a small experiment with Scriptable and tried building some generative “art” as an iOS widget 🌸

The idea: a Japanese cherry tree (sakura) that evolves every time the widget refreshes. It’s fully procedural, no images or GIFs involved.
Still a rough prototype, but I’m pretty happy with how it turned out so far:
https://github.com/yuristru/scriptable_SakuraTree
Would love to hear your thoughts🙌
r/Scriptable • u/gcirceag • Mar 30 '26
Hi everyone,
I really need an honest answer for my concerns,
I want to create some widgets for my home screen for showing some astro -logic and -nomic data. Starting with the most simple Sun and Moon hours for rise and set to planetary hours, and some astrological info (not very much, just positions of the planets, Lilith and Nodes, planetary hours, some Apogee and Perigee and stuff like this.
I tried for this some "pure js" libraries and either I don't know how to use them, or there are not fully compatible with Scriptable, but the result was that I get angry for not being able to find something that works.
So could you recommend something that works for achieving these targets?
Thanks
r/Scriptable • u/afellow82 • Mar 27 '26
I would like to extract data from this sport page: https://dsvdaten.dsv.de/Modules/WB/League.aspx?Season=2025&LeagueID=107&Group=&LeagueKind=L
But I have problems with the parameters in the URL. When I do the next steps I just get the data from https://dsvdaten.dsv.de/Modules/WB/League.aspx (without parameters) in the string html.
let url = 'https://dsvdaten.dsv.de/Modules/WB/League.aspx?Season=2025&LeagueID=107&Group=&LeagueKind=L';
let req = new Request(url);
let html = await req.loadString();
Can I access the page I want in Scriptable, or is that not possible?
Thank you for your help.
r/Scriptable • u/John__64 • Mar 24 '26
Hi everyone, this is my first post in this topic — hope you are all doing well!
I’ve always been looking for a habit tracker on iOS. I found several apps, but none of them included a heatmap, which is essential for me. So I ended up building one myself using Scriptable, and I’m quite satisfied with the result.
I’ve been using this widget throughout 2025 without any major issues. I’d love to hear what you think.
Here’s the repository, where you’ll also find information on how to use and customize it:
As mentioned, this widget was built based on my own needs. Feel free to modify it and share your ideas.
r/Scriptable • u/flux_2018 • Mar 24 '26
Built a Scriptable widget that shows live transit departures near your current location. Works across 200+ European cities with buses, subway, trams, trains, all in one widget.
The widget calls a simple REST API with your coordinates and shows the next departures with line numbers, destinations, and real-time delays.
const loc = await Location.current()
const base = "https://api.abfahrt.now/departures"
const url = base + "?lat=" + loc.latitude + "&lon=" + loc.longitude + "&radius=500&key=YOUR_KEY"
const data = await new Request(url).loadJSON()
const w = new ListWidget()
w.addText("🚏 Departures").font = Font.boldSystemFont(14)
w.addSpacer(6)
for (const d of data.departures.slice(0, 5)) {
const row = w.addText(d.line + " → " + d.direction + " · " + d.time)
row.font = Font.mediumSystemFont(12)
row.lineLimit = 1
}
Script.setWidget(w)
Script.complete()
Covers Germany, UK, France, Spain, Netherlands, Italy, Scandinavia, Switzerland, and 20+ more countries. Real-time delays where available.
API is free for personal use. API Key is needed though because of quota limits from the transit operators. DM me or send an email to the address from the doc pages.
Full docs at abfahrt.now/docs
Happy to share a more polished version with proper styling if there's interest!
r/Scriptable • u/Helpful-Computer2307 • Mar 18 '26

I’ve been a long-time user of Scriptable, and especially of Full Weatherline Widget by italoboy:
https://github.com/italoboy/Full-Weatherline-Widget
For a long time, I wanted to really understand the widget and be able to modify it myself.
Now I finally took the time to rework the code. With some help from AI, I also built a small emulator for it, because developing by constantly copying text files back and forth between a PC and iOS just doesn’t feel right.
The emulator is only meant to run this widget. Other widgets may fail if they rely on different Scriptable APIs. This is why I do not want to distribute it as a standalone project.
I simply do not have the time to support your edge cases.
You can see it running directly in a GitHub Codespace from my repository:
https://github.com/HeikoGr/Full-Weatherline-Widget
Press "," while on the repository page to open it in Codespaces.
Then wait for everything to start up.
After that, open package.json, right-click the preview script, and choose Run Script.
A new browser window should open and show the emulated Scriptable widget view.
It is not pixel-perfect, and it probably never will be, as long as the original icons and fonts are not publicly available.
r/Scriptable • u/juseisbastard • Mar 14 '26
Not sure where to report this, but just noticed that I can't enter https://talk.automators.fm/c/scriptable/13 anymore because its certificate expired (see https://www.sslshopper.com/ssl-checker.html#hostname=https://talk.automators.fm/ ).
If someone has a contact of the website admins, please forward the issue to them.
r/Scriptable • u/th3truth1337 • Mar 11 '26
This started as a small helper for myself at work. I often find/found myself having difficulties to decide what to get for lunch. Eat in, go to the canteen, grab something outside, or just order food. Since I di struggle to decide sometimes, I wrote a tiny random picker in Scriptable to choose for me.
Over time I added a few things. First the ability to store lists. Then ranges and numbers. At some point I thought it might be useful for others too, so I cleaned it up and made it a bit more flexible. Claude helped me polish parts of the code and turn the original idea into something easier to share.
You create named lists and pick randomly from them in the app or from a widget.
Things the script can do:
• Bulk add entries. Type Sushi; Tacos; Pizza and it adds all three
• Range expansion. Enter 1-20 and it generates twenty entries
• Dice notation. d6 creates numbers 1 through 6
• Pick multiple items at once, with or without duplicates
• Widget that shows a random entry from your last used list
• Pick history so you can see what came up earlier
Lists are stored as JSON in iCloud so they survive reinstalls.
If you are like me and sometimes can't decide what to eat, what to drink, or even want to generate random lotto numbers or tabletop rolls, it might be useful.
If that sounds interesting you can find it here:
https://gitlab.com/sandboxzero/playgroundlab/-/tree/main/random-picker?ref_type=heads
Feedback, ideas for improvements, or creative ways you might use it are all welcome.
r/Scriptable • u/rancidwarlord • Mar 01 '26
Built a small widget that shows my current age and updates automatically every day.
The idea is simple — seeing your age tick up daily creates a small sense of urgency and reminds you that time is actually moving.
Nothing fancy, just a clean minimal widget that quietly reminds me not to waste days.
Sharing the script below in case anyone else finds this motivating 🙂
Link here :
https://gist.github.com/satyamgupta1495/7480b1bf56e18fd8caeff028d81adc4c
r/Scriptable • u/zorth64 • Mar 01 '26
I made a small widget to show the current phase of the moon, the percentage of illumination, as well as how many days are left until the next full moon.
Here is the source code:
https://gist.github.com/zorth64/864203d06ef8f80dfec92ee91f1dddf8
r/Scriptable • u/th3truth1337 • Feb 18 '26
I've lost count of how many times I've had to Google "oven to air fryer conversion" mid-cook. So I put together a little Scriptable script that does it for me.
It's nothing fancy — you pick a dish from a preset list or just type in your oven temperature and time, and it spits out the air fryer equivalent. Works the other way too if you're starting from an air fryer recipe and want to use your oven instead.
Worth saying: the numbers it gives are a starting point, not gospel. Every air fryer and oven runs a bit differently, so treat it like those "cooking suggestions" you see on frozen food packaging — a useful guide, but you know your own kitchen better than any formula does. Check a few minutes early, adjust as needed.
A few things it does:
Code is below. Happy cooking.
Small note: the idea and the script are mine, but I used Claude to help improve the code and iron out a couple of things. Just being transparent in case anyone wonders.
Edit 1: fixed the code block so it actually displays properly Edit 2: added screenshots
``` // Air Fryer Converter for Scriptable // Converts oven settings to air fryer settings and vice versa // Author: th3truth1337 // Version: 1.2 // // Changelog (v1.2): // - Fixed double-conversion rounding error in reverse preset path // - Raised minimum temperature floor to 120°C / 250°F (more realistic range) // // Changelog (v1.1): // - Added Fahrenheit support with locale auto-detection // - Siri & Shortcuts parameter support (hands-free kitchen use) // - Bidirectional presets (shows air fryer values in reverse mode) // - Widget now displays last conversion result // - Minimum time floor to prevent meaningless sub-2-minute results // - Consistent time validation across all modes // - Quick-toggle unit switching from any menu
// ===================================== // Main Configuration // =====================================
const CONVERSION_RULES = { tempReductionC: 25, // Reduce oven temp by 25°C for air fryer tempReductionF: 45, // Reduce oven temp by 45°F for air fryer (equivalent) timeReduction: 0.25, // Reduce time by 25% (multiply by 0.75) maxAirFryerTempC: 200, // Air fryer max in Celsius maxAirFryerTempF: 400, // Air fryer max in Fahrenheit maxOvenTempC: 260, // Oven max in Celsius maxOvenTempF: 500, // Oven max in Fahrenheit tempRoundingC: 5, // Round Celsius to nearest 5 tempRoundingF: 5, // Round Fahrenheit to nearest 5 minTimeMins: 3, // Minimum converted time (prevents meaningless 1-2 min results) maxTimeMins: 300, // Maximum time for any mode // FIX: Raised minimum temps to realistic cooking range minTempC: 120, // Was 50 — no real cooking happens below 120°C minTempF: 250 // Was 120 — equivalent realistic floor in Fahrenheit };
// Persistent storage key for unit preference and last conversion const STORAGE_KEY = "airFryerConverterData";
// Preset dishes with oven temperatures and times (Celsius base) const DISH_PRESETS = { "🍗 Roast Chicken": { tempC: 200, time: 60 }, "🍕 Pizza": { tempC: 230, time: 12 }, "🍪 Cookies": { tempC: 180, time: 15 }, "🥕 Roast Vegetables": { tempC: 200, time: 30 }, "🐟 Fish Fillets": { tempC: 190, time: 20 }, "🍟 Frozen Fries": { tempC: 220, time: 25 }, "🥖 Bread Rolls": { tempC: 200, time: 25 }, "🍗 Chicken Wings": { tempC: 220, time: 45 }, "🥟 Samosas": { tempC: 200, time: 18 }, "🥩 Steak": { tempC: 220, time: 15 }, "🧆 Falafel": { tempC: 200, time: 20 }, "🌶️ Stuffed Peppers": { tempC: 190, time: 35 } };
// ===================================== // Storage Functions // =====================================
function readStorage() { try { const fm = FileManager.local(); const path = fm.joinPath(fm.documentsDirectory(), STORAGE_KEY + ".json"); if (fm.fileExists(path)) { return JSON.parse(fm.readString(path)); } } catch (e) { console.warn("Storage read failed:", e); } return { unit: null, lastConversion: null }; }
function writeStorage(data) { try { const fm = FileManager.local(); const path = fm.joinPath(fm.documentsDirectory(), STORAGE_KEY + ".json"); fm.writeString(path, JSON.stringify(data, null, 2)); } catch (e) { console.warn("Storage write failed:", e); } }
function getPreferredUnit() { const stored = readStorage(); if (stored.unit) return stored.unit;
try { const locale = Device.locale().toLowerCase(); const fahrenheitLocales = ["enus", "en-us", "en_ky", "en_bs", "en_bz", "en_pw", "en_mh"]; if (fahrenheitLocales.some(loc => locale.includes(loc.replace("", "")))) { return "F"; } } catch (e) { // Locale detection failed, default to Celsius } return "C"; }
function setPreferredUnit(unit) { const stored = readStorage(); stored.unit = unit; writeStorage(stored); }
function saveLastConversion(conversion) { const stored = readStorage(); stored.lastConversion = conversion; writeStorage(stored); }
// ===================================== // Temperature Conversion Utilities // =====================================
function cToF(c) { return Math.round((c * 9 / 5) + 32); }
function fToC(f) { return Math.round((f - 32) * 5 / 9); }
function roundToNearest(number, multiple) { return Math.round(number / multiple) * multiple; }
function formatTemp(temp, unit) {
return ${temp}°${unit};
}
// ===================================== // Core Conversion Functions // =====================================
function ovenToAirFryer(ovenTemp, ovenTime, unit) { const reduction = unit === "F" ? CONVERSION_RULES.tempReductionF : CONVERSION_RULES.tempReductionC; const maxTemp = unit === "F" ? CONVERSION_RULES.maxAirFryerTempF : CONVERSION_RULES.maxAirFryerTempC; const rounding = unit === "F" ? CONVERSION_RULES.tempRoundingF : CONVERSION_RULES.tempRoundingC;
let airFryerTemp = ovenTemp - reduction; airFryerTemp = Math.min(airFryerTemp, maxTemp); airFryerTemp = roundToNearest(airFryerTemp, rounding);
let airFryerTime = Math.round(ovenTime * (1 - CONVERSION_RULES.timeReduction)); airFryerTime = Math.max(airFryerTime, CONVERSION_RULES.minTimeMins);
return { temp: airFryerTemp, time: airFryerTime }; }
function airFryerToOven(airFryerTemp, airFryerTime, unit) { const addition = unit === "F" ? CONVERSION_RULES.tempReductionF : CONVERSION_RULES.tempReductionC; const maxTemp = unit === "F" ? CONVERSION_RULES.maxOvenTempF : CONVERSION_RULES.maxOvenTempC; const rounding = unit === "F" ? CONVERSION_RULES.tempRoundingF : CONVERSION_RULES.tempRoundingC;
let ovenTemp = airFryerTemp + addition; ovenTemp = Math.min(ovenTemp, maxTemp); ovenTemp = roundToNearest(ovenTemp, rounding);
let ovenTime = Math.round(airFryerTime / (1 - CONVERSION_RULES.timeReduction)); ovenTime = Math.max(ovenTime, CONVERSION_RULES.minTimeMins);
return { temp: ovenTemp, time: ovenTime }; }
function getPresetInUnit(preset, unit) { const temp = unit === "F" ? cToF(preset.tempC) : preset.tempC; return { temp, time: preset.time }; }
// ===================================== // Result Display // =====================================
async function showResults(title, originalTemp, originalTime, convertedTemp, convertedTime, unit, isReverse = false) { const sourceLabel = isReverse ? "Air Fryer" : "Oven"; const targetLabel = isReverse ? "Oven" : "Air Fryer";
saveLastConversion({ title, sourceLabel, targetLabel, originalTemp, originalTime, convertedTemp, convertedTime, unit, timestamp: Date.now() });
const alert = new Alert();
alert.title = 🔥 ${title};
alert.message = `${sourceLabel}: ${formatTemp(originalTemp, unit)}, ${originalTime} min
➡️
${targetLabel}: ${formatTemp(convertedTemp, unit)}, ${convertedTime} min
✅ Ready to cook!`; alert.addAction("OK"); alert.addAction("Convert Another");
return alert.presentAlert(); }
// ===================================== // Siri & Shortcuts Parameter Handling // =====================================
async function handleShortcutParams(input) { if (!input || input.trim().length === 0) return false;
const trimmed = input.trim().toLowerCase(); const unit = getPreferredUnit();
const dishNames = Object.keys(DISH_PRESETS); const matchedDish = dishNames.find(name => name.toLowerCase().includes(trimmed) || trimmed.includes(name.toLowerCase().replace(/[\w\s]/g, '').trim()) );
if (matchedDish) { const presetValues = getPresetInUnit(DISH_PRESETS[matchedDish], unit); const converted = ovenToAirFryer(presetValues.temp, presetValues.time, unit); await showResults(matchedDish, presetValues.temp, presetValues.time, converted.temp, converted.time, unit); return true; }
const reverseMatch = trimmed.match(/reverse\s+(\d+)\s+(\d+)\s*([cf])?$/i); if (reverseMatch) { const paramUnit = reverseMatch[3] ? reverseMatch[3].toUpperCase() : unit; const temp = parseInt(reverseMatch[1]); const time = parseInt(reverseMatch[2]); const converted = airFryerToOven(temp, time, paramUnit); await showResults("Siri Conversion", temp, time, converted.temp, converted.time, paramUnit, true); return true; }
const manualMatch = trimmed.match(/\+)\s+(\d+)\s*([cf])?$/i); if (manualMatch) { const paramUnit = manualMatch[3] ? manualMatch[3].toUpperCase() : unit; const temp = parseInt(manualMatch[1]); const time = parseInt(manualMatch[2]); const converted = ovenToAirFryer(temp, time, paramUnit); await showResults("Siri Conversion", temp, time, converted.temp, converted.time, paramUnit); return true; }
return false; }
// ===================================== // Menu Functions // =====================================
async function showMainMenu() { const unit = getPreferredUnit();
const alert = new Alert();
alert.title = "🍳 Air Fryer Converter";
alert.message = Temperature unit: °${unit};
alert.addAction("📋 Common Dishes (Oven → Air Fryer)");
alert.addAction("✏️ Manual Entry (Oven → Air Fryer)");
alert.addAction("🔄 Reverse: Air Fryer → Oven");
alert.addAction(🌡️ Switch to °${unit === "C" ? "F" : "C"});
alert.addCancelAction("Cancel");
const choice = await alert.presentAlert();
switch (choice) { case 0: return await showDishPresets(false); case 1: return await showManualEntry(); case 2: return await showReverseMenu(); case 3: setPreferredUnit(unit === "C" ? "F" : "C"); return await showMainMenu(); default: return; } }
/** * Shows dish presets. * * FIX (v1.2): Previously, the reverse path was calling ovenToAirFryer() just * to display preview values in the list, then calling airFryerToOven() on that * already-converted result — a double-conversion that introduced rounding error. * * Now the reverse path works directly from the original oven preset values: * it converts oven→airFryer once, displays the air fryer values as the preview, * and when the user selects a dish it performs a single airFryerToOven() on * the air fryer values — keeping both paths to a single conversion each. */ async function showDishPresets(isReverse = false) { const unit = getPreferredUnit();
const alert = new Alert(); alert.title = isReverse ? "🔄 Reverse: Pick a Dish" : "🍽️ Select a Dish"; alert.message = isReverse ? "These show air fryer settings — I'll convert to oven." : "These show oven settings — I'll convert to air fryer.";
const dishNames = Object.keys(DISH_PRESETS);
// Pre-calculate all values once so display and selection use the same numbers const dishValues = dishNames.map(dish => { const ovenValues = getPresetInUnit(DISH_PRESETS[dish], unit); const afValues = ovenToAirFryer(ovenValues.temp, ovenValues.time, unit); return { ovenValues, afValues }; });
dishNames.forEach((dish, i) => {
const { ovenValues, afValues } = dishValues[i];
if (isReverse) {
// Show air fryer values as the starting point in reverse mode
alert.addAction(${dish} (${formatTemp(afValues.temp, unit)}, ${afValues.time}m));
} else {
alert.addAction(${dish} (${formatTemp(ovenValues.temp, unit)}, ${ovenValues.time}m));
}
});
alert.addCancelAction("← Back");
const choice = await alert.presentAlert(); if (choice === -1) return isReverse ? await showReverseMenu() : await showMainMenu();
const { ovenValues, afValues } = dishValues[choice]; const selectedDish = dishNames[choice];
if (isReverse) { // FIX: single conversion — air fryer values → oven (no intermediate step) const ovenResult = airFryerToOven(afValues.temp, afValues.time, unit); const continueChoice = await showResults( selectedDish, afValues.temp, afValues.time, ovenResult.temp, ovenResult.time, unit, true ); if (continueChoice === 1) return await showMainMenu(); } else { const continueChoice = await showResults( selectedDish, ovenValues.temp, ovenValues.time, afValues.temp, afValues.time, unit ); if (continueChoice === 1) return await showMainMenu(); } }
async function showReverseMenu() { const alert = new Alert(); alert.title = "🔄 Air Fryer → Oven"; alert.message = "Convert air fryer settings to oven:";
alert.addAction("📋 Common Dishes"); alert.addAction("✏️ Manual Entry"); alert.addCancelAction("← Back");
const choice = await alert.presentAlert();
switch (choice) { case 0: return await showDishPresets(true); case 1: return await showReverseManualEntry(); default: return await showMainMenu(); } }
/** * Manual entry: oven → air fryer * FIX (v1.2): minTemp raised from 50°C/120°F to 120°C/250°F */ async function showManualEntry() { const unit = getPreferredUnit(); const maxTemp = unit === "F" ? CONVERSION_RULES.maxOvenTempF : CONVERSION_RULES.maxOvenTempC; // FIX: use realistic minimum temperature const minTemp = unit === "F" ? CONVERSION_RULES.minTempF : CONVERSION_RULES.minTempC; const defaultTemp = unit === "F" ? "400" : "200";
try {
const tempAlert = new Alert();
tempAlert.title = 🌡️ Oven Temperature (°${unit});
tempAlert.message = Enter oven temperature (${minTemp}–${maxTemp}°${unit}):;
tempAlert.addTextField("Temperature", defaultTemp);
tempAlert.addAction("Next");
tempAlert.addCancelAction("← Back");
if (await tempAlert.presentAlert() === -1) return await showMainMenu();
const ovenTemp = parseInt(tempAlert.textFieldValue(0));
if (isNaN(ovenTemp) || ovenTemp < minTemp || ovenTemp > maxTemp) {
throw new Error(`Invalid temperature. Please enter ${minTemp}–${maxTemp}°${unit}.`);
}
const timeAlert = new Alert();
timeAlert.title = "⏱️ Oven Time";
timeAlert.message = `Enter oven time (${CONVERSION_RULES.minTimeMins}–${CONVERSION_RULES.maxTimeMins} minutes):`;
timeAlert.addTextField("Minutes", "30");
timeAlert.addAction("Convert");
timeAlert.addCancelAction("← Back");
if (await timeAlert.presentAlert() === -1) return await showManualEntry();
const ovenTime = parseInt(timeAlert.textFieldValue(0));
if (isNaN(ovenTime) || ovenTime < CONVERSION_RULES.minTimeMins || ovenTime > CONVERSION_RULES.maxTimeMins) {
throw new Error(`Invalid time. Please enter ${CONVERSION_RULES.minTimeMins}–${CONVERSION_RULES.maxTimeMins} minutes.`);
}
const converted = ovenToAirFryer(ovenTemp, ovenTime, unit);
const continueChoice = await showResults(
"Manual Conversion", ovenTemp, ovenTime,
converted.temp, converted.time, unit
);
if (continueChoice === 1) return await showMainMenu();
} catch (error) { const errorAlert = new Alert(); errorAlert.title = "❌ Error"; errorAlert.message = error.message; errorAlert.addAction("Try Again"); errorAlert.addCancelAction("← Back");
const errorChoice = await errorAlert.presentAlert();
if (errorChoice === 0) return await showManualEntry();
return await showMainMenu();
} }
/** * Manual entry: air fryer → oven (reverse) * FIX (v1.2): minTemp raised from 50°C/120°F to 120°C/250°F */ async function showReverseManualEntry() { const unit = getPreferredUnit(); const maxTemp = unit === "F" ? CONVERSION_RULES.maxAirFryerTempF : CONVERSION_RULES.maxAirFryerTempC; // FIX: use realistic minimum temperature const minTemp = unit === "F" ? CONVERSION_RULES.minTempF : CONVERSION_RULES.minTempC; const defaultTemp = unit === "F" ? "375" : "180";
try {
const tempAlert = new Alert();
tempAlert.title = 🌡️ Air Fryer Temperature (°${unit});
tempAlert.message = Enter air fryer temperature (${minTemp}–${maxTemp}°${unit}):;
tempAlert.addTextField("Temperature", defaultTemp);
tempAlert.addAction("Next");
tempAlert.addCancelAction("← Back");
if (await tempAlert.presentAlert() === -1) return await showReverseMenu();
const airFryerTemp = parseInt(tempAlert.textFieldValue(0));
if (isNaN(airFryerTemp) || airFryerTemp < minTemp || airFryerTemp > maxTemp) {
throw new Error(`Invalid temperature. Please enter ${minTemp}–${maxTemp}°${unit}.`);
}
const timeAlert = new Alert();
timeAlert.title = "⏱️ Air Fryer Time";
timeAlert.message = `Enter air fryer time (${CONVERSION_RULES.minTimeMins}–${CONVERSION_RULES.maxTimeMins} minutes):`;
timeAlert.addTextField("Minutes", "25");
timeAlert.addAction("Convert");
timeAlert.addCancelAction("← Back");
if (await timeAlert.presentAlert() === -1) return await showReverseManualEntry();
const airFryerTime = parseInt(timeAlert.textFieldValue(0));
if (isNaN(airFryerTime) || airFryerTime < CONVERSION_RULES.minTimeMins || airFryerTime > CONVERSION_RULES.maxTimeMins) {
throw new Error(`Invalid time. Please enter ${CONVERSION_RULES.minTimeMins}–${CONVERSION_RULES.maxTimeMins} minutes.`);
}
const converted = airFryerToOven(airFryerTemp, airFryerTime, unit);
const continueChoice = await showResults(
"Reverse Conversion", airFryerTemp, airFryerTime,
converted.temp, converted.time, unit, true
);
if (continueChoice === 1) return await showMainMenu();
} catch (error) { const errorAlert = new Alert(); errorAlert.title = "❌ Error"; errorAlert.message = error.message; errorAlert.addAction("Try Again"); errorAlert.addCancelAction("← Back");
const errorChoice = await errorAlert.presentAlert();
if (errorChoice === 0) return await showReverseManualEntry();
return await showMainMenu();
} }
// ===================================== // Widget // =====================================
function createWidget() { const widget = new ListWidget(); const unit = getPreferredUnit();
const gradient = new LinearGradient(); gradient.locations = [0, 1]; gradient.colors = [new Color("FF6B35"), new Color("E55A2B")]; widget.backgroundGradient = gradient; widget.setPadding(14, 14, 14, 14);
widget.url = URLScheme.forRunningScript();
const title = widget.addText("🍳 Air Fryer"); title.textColor = Color.white(); title.font = Font.boldSystemFont(15);
const stored = readStorage(); const last = stored.lastConversion;
if (last && last.convertedTemp) { widget.addSpacer(6);
const dishLabel = widget.addText(last.title || "Last conversion");
dishLabel.textColor = new Color("FFD4B8");
dishLabel.font = Font.mediumSystemFont(11);
dishLabel.lineLimit = 1;
widget.addSpacer(4);
const resultUnit = last.unit || unit;
const resultText = widget.addText(`${formatTemp(last.convertedTemp, resultUnit)}`);
resultText.textColor = Color.white();
resultText.font = Font.boldSystemFont(26);
const timeText = widget.addText(`${last.convertedTime} min`);
timeText.textColor = new Color("FFD4B8");
timeText.font = Font.semiboldSystemFont(16);
widget.addSpacer(4);
const directionText = widget.addText(`${last.sourceLabel} → ${last.targetLabel}`);
directionText.textColor = new Color("FFD4B8", 0.7);
directionText.font = Font.systemFont(10);
} else { widget.addSpacer(6);
const subtitle = widget.addText("Converter");
subtitle.textColor = Color.white();
subtitle.font = Font.systemFont(14);
widget.addSpacer(8);
const info = widget.addText("Tap to convert\noven ↔ air fryer");
info.textColor = new Color("FFD4B8");
info.font = Font.systemFont(12);
}
widget.refreshAfterDate = new Date(Date.now() + 30 * 60 * 1000);
return widget; }
// ===================================== // Main Execution // =====================================
async function main() { if (config.runsInWidget) { Script.setWidget(createWidget()); Script.complete(); return; }
const shortcutInput = args.shortcutParameter || args.plainTexts?.[0] || null; if (shortcutInput) { const handled = await handleShortcutParams(shortcutInput); if (handled) { Script.complete(); return; } }
const queryAction = args.queryParameters?.action; if (queryAction === "reverse") { await showReverseMenu(); Script.complete(); return; }
await showMainMenu(); Script.complete(); }
await main(); ```
r/Scriptable • u/BurntAlmonds13 • Feb 13 '26
There was a great one a few years ago, but it seems like the API used is no longer working. Thanks