Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 93 additions & 24 deletions tools/widget/scriptable-waits.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,61 @@
// Magic Monitor — iOS home-screen waits widget (Scriptable).
//
// Shows ONE park's waits (a home-screen widget shouldn't try to be the
// whole park map). Which park:
// 1. If you have an ACTIVE PLAN today → that park (auto — the "right
// park for the day", no config).
// 2. Else the park you PIN via the widget Parameter (see setup #5).
// 3. Else your park with the most favorites (never blank).
// Plus DOWN rides flagged and current temp/conditions at WDW.
//
// Setup (one time):
// 1. Install "Scriptable" from the App Store.
// 2. New script → paste this file → name it "MM Waits".
// 3. Sign into magicmonitor.megillini.dev/waits on your phone, expand
// "Phone widget setup", copy your private feed URL, and paste it
// into FEED_URL below. Treat that URL like a password.
// 4. Long-press home screen → add a Scriptable widget (medium works
// best) → choose "MM Waits".
// 4. Long-press home screen → add a Scriptable widget → choose
// "MM Waits". Small fits ~5 rides; medium/large fit more.
// 5. (Optional) Pin a park: long-press the widget → Edit Widget →
// Parameter → type a park (epcot, MK, "hollywood studios", AK…).
// Leave it blank to auto-follow today's plan.
//
// Shows: today's active plan (if any) or your favorites, DOWN rides
// flagged, plus current temp/conditions at WDW. iOS refreshes widgets on
// its own cadence (~every 5–15 min); tap the widget to open /waits for
// live-now numbers.
// iOS refreshes widgets on its own cadence (~5–15 min); tap to open
// /waits for live-now numbers.

const FEED_URL = "PASTE_YOUR_FEED_URL_HERE";

const MAX_ROWS = 6;
// How many ride rows fit, by widget size.
function rideBudget() {
switch (config.widgetFamily) {
case "small": return 5;
case "large": return 20;
default: return 10; // medium (and the in-app preview)
}
}

// Loose match of a pinned-park string to a feed park group. Accepts the
// key (magic_kingdom), the name (Magic Kingdom), or a short code (MK).
const SHORT_CODES = {
mk: "magic_kingdom",
ep: "epcot",
epcot: "epcot",
hs: "hollywood_studios",
dhs: "hollywood_studios",
ak: "animal_kingdom",
};
function matchPark(groups, raw) {
if (!raw) return null;
const q = raw.trim().toLowerCase();
const code = SHORT_CODES[q];
return (
groups.find((g) => g.park_key === q) ||
groups.find((g) => g.park_name.toLowerCase() === q) ||
(code && groups.find((g) => g.park_key === code)) ||
groups.find((g) => g.park_name.toLowerCase().includes(q)) ||
null
);
}

async function run() {
const w = new ListWidget();
Expand All @@ -35,38 +74,60 @@ async function run() {
return finish(w);
}

// Header: title + weather.
// Decide the single park + its rides. Plan wins; then pinned param;
// then largest favorites group.
const planActive = data.plan && data.plan.rides.length > 0;
const groups = data.parks ?? [];
let parkLabel, rides, planning = false;

if (planActive) {
planning = true;
parkLabel = data.plan.park_name;
rides = data.plan.rides.map((r, i) => ({ ...r, prefix: `${i + 1}. ` }));
} else {
const pinned = matchPark(groups, args.widgetParameter);
const group =
pinned ||
[...groups].sort((a, b) => b.rides.length - a.rides.length)[0];
if (group) {
parkLabel = group.park_name;
rides = group.rides;
}
}

// Header: park (or brand) on the left, weather on the right.
const head = w.addStack();
head.centerAlignContent();
const title = head.addText("Magic Monitor");
title.font = Font.boldSystemFont(12);
const title = head.addText(parkLabel || "Magic Monitor");
title.font = Font.boldSystemFont(13);
title.textColor = new Color("#d4af37");
title.lineLimit = 1;
head.addSpacer();
if (data.weather) {
const wx = head.addText(
`${data.weather.icon} ${data.weather.temp_f}°`,
);
const wx = head.addText(`${data.weather.icon} ${data.weather.temp_f}°`);
wx.font = Font.systemFont(12);
wx.textColor = Color.white();
}
w.addSpacer(6);

// Prefer the active plan (it's "what's next"); fall back to favorites.
let rows = [];
if (data.plan && data.plan.rides.length > 0) {
rows = data.plan.rides.map((r, i) => ({ ...r, prefix: `${i + 1}. ` }));
} else {
rows = (data.parks ?? []).flatMap((g) => g.rides);
if (planning) {
const sub = w.addText("Today's plan");
sub.font = Font.mediumSystemFont(9);
sub.textColor = new Color("#8a8378");
}
w.addSpacer(6);

if (rows.length === 0) {
const none = w.addText("No favorites picked yet — tap to set up.");
if (!rides || rides.length === 0) {
const none = w.addText(
groups.length === 0
? "No favorites picked yet — tap to set up."
: "No rides for that park.",
);
none.font = Font.systemFont(11);
none.textColor = Color.gray();
return finish(w);
}

for (const r of rows.slice(0, MAX_ROWS)) {
const budget = rideBudget();
for (const r of rides.slice(0, budget)) {
const line = w.addStack();
line.centerAlignContent();
const name = line.addText(`${r.prefix ?? ""}${r.ride_name}`);
Expand All @@ -91,6 +152,14 @@ async function run() {
w.addSpacer(3);
}

// Show when we've trimmed, so a hidden ride isn't a silent surprise.
if (rides.length > budget) {
w.addSpacer(2);
const more = w.addText(`+${rides.length - budget} more — tap`);
more.font = Font.systemFont(9);
more.textColor = new Color("#8a8378");
}

return finish(w);
}

Expand Down
Loading