diff --git a/tools/widget/scriptable-waits.js b/tools/widget/scriptable-waits.js index ea7a19a..55f16ec 100644 --- a/tools/widget/scriptable-waits.js +++ b/tools/widget/scriptable-waits.js @@ -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(); @@ -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}`); @@ -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); }