// Variables used by Scriptable. // These must be at the very top of the file. Do not edit. // icon-color: blue; icon-glyph: train; // ------------------------------------------------------------- // CONFIGURATION // ------------------------------------------------------------- // URL deines Backends (ohne abschließenden Slash) const BASE_URL = "https://dein-server.de"; // Deine Client-ID aus der App (oder als Widget-Parameter übergeben!) // Wenn Du im Homescreen lange auf das Widget drückst und "Widget bearbeiten" // auswählst, kannst Du die Client-ID im Feld "Parameter" eintragen. const FALLBACK_CLIENT_ID = "client_..."; // ------------------------------------------------------------- const widgetArgs = args.widgetParameter ? args.widgetParameter.trim() : FALLBACK_CLIENT_ID; const parts = widgetArgs.split("|"); const clientId = parts[0] || ""; const widgetMode = parts.length > 1 ? parts[1].toLowerCase() : "perm"; // "temp" or "perm" const isLarge = config.widgetFamily === "large"; const isMedium = config.widgetFamily === "medium" || isLarge; const isSmall = config.widgetFamily === "small"; const rowCount = isLarge ? 12 : 5; async function fetchDepartures() { if (!clientId || clientId === "client_...") { return { error: "Keine Client-ID konfiguriert. Bitte als Widget-Parameter eintragen." }; } try { const url = `${BASE_URL}/api/ha/subscriptions?client_id=${encodeURIComponent(clientId)}&count=${rowCount}`; let req = new Request(url); req.timeoutInterval = 30; const res = await req.loadJSON(); return res; } catch (err) { console.error(err); // Check if it's the standard iOS JSON parsing error, which implies HTML/text was returned. let errorMsg = String(err.message || err); if (errorMsg.includes("Format") || errorMsg.includes("format") || errorMsg.includes("JSON") || errorMsg.includes("read")) { return { error: "Backend liefert kein JSON (z.B. falsche URL oder 500 Fehler)." }; } return { error: "Fehler beim Laden der Abfahrten." }; } } function drawError(widget, error) { let errorString = error; if (typeof error === 'object') { errorString = error.message || JSON.stringify(error); } const errText = widget.addText(String(errorString)); errText.font = Font.systemFont(10); errText.textColor = Color.red(); } function drawTimeStack(rowStack, d) { const timeStack = rowStack.addStack(); timeStack.layoutHorizontally(); timeStack.size = new Size(40, 0); if (d.missing) { const planText = timeStack.addText(d.plan_time); planText.font = Font.systemFont(11); planText.textColor = Color.dynamic(new Color("#888888"), new Color("#888888")); timeStack.addSpacer(1); const missingText = timeStack.addText("Fehlt"); missingText.font = Font.boldSystemFont(10); missingText.textColor = Color.orange(); } else if (d.partial) { const planText = timeStack.addText(d.plan_time); planText.font = Font.systemFont(11); planText.textColor = Color.dynamic(new Color("#888888"), new Color("#888888")); timeStack.addSpacer(1); const partialText = timeStack.addText("Teil"); partialText.font = Font.boldSystemFont(10); partialText.textColor = Color.orange(); } else if (d.cancelled) { const planText = timeStack.addText(d.plan_time); planText.font = Font.systemFont(11); planText.textColor = Color.dynamic(new Color("#888888"), new Color("#888888")); timeStack.addSpacer(1); const cancelText = timeStack.addText("Ausfall"); cancelText.font = Font.boldSystemFont(10); cancelText.textColor = Color.red(); } else if (d.versp_min > 0) { const planText = timeStack.addText(d.plan_time); planText.font = Font.systemFont(11); planText.textColor = Color.dynamic(new Color("#888888"), new Color("#888888")); timeStack.addSpacer(2); const istText = timeStack.addText(d.ist_time); istText.font = Font.boldSystemFont(11); istText.textColor = Color.red(); } else { const planText = timeStack.addText(d.plan_time); planText.font = Font.boldSystemFont(11); planText.textColor = Color.dynamic(Color.black(), Color.white()); } } function drawInfoStack(rowStack, d) { const infoStack = rowStack.addStack(); infoStack.layoutVertically(); const topInfo = infoStack.addStack(); topInfo.layoutHorizontally(); const lineText = topInfo.addText(d.linie); lineText.font = Font.boldSystemFont(11); if (d.missing) { lineText.textColor = Color.orange(); } else if (d.partial) { lineText.textColor = Color.orange(); } else if (d.cancelled) { lineText.textColor = Color.dynamic(new Color("#c23b22"), new Color("#ff6961")); } else if (d.versp_min >= 10) { lineText.textColor = Color.dynamic(new Color("#cc5500"), new Color("#ffb347")); } else if (d.versp_min >= 5) { lineText.textColor = Color.dynamic(new Color("#d4af37"), new Color("#fdfd96")); } else { lineText.textColor = Color.dynamic(new Color("#228b22"), new Color("#77dd77")); } topInfo.addSpacer(4); const dirText = topInfo.addText(d.partial ? `nur bis ${d.partial_destination || d.richtung}` : d.richtung); dirText.font = Font.systemFont(11); dirText.textColor = Color.dynamic(Color.black(), Color.white()); dirText.lineLimit = 1; const botInfo = infoStack.addStack(); botInfo.layoutHorizontally(); botInfo.centerAlignContent(); const trackText = botInfo.addText(d.missing ? "Nicht gefunden" : (d.partial ? "Teilstrecke" : `Gl. ${d.gleis}`)); trackText.font = Font.systemFont(9); trackText.textColor = Color.dynamic(new Color("#666666"), new Color("#aaaaaa")); if (d.wagen > 0) { botInfo.addSpacer(4); const wagonStack = botInfo.addStack(); wagonStack.centerAlignContent(); wagonStack.spacing = 1; for (let w = 0; w < d.wagen; w++) { const rect = wagonStack.addStack(); rect.size = new Size(4, 2); rect.cornerRadius = 1; rect.backgroundColor = Color.dynamic(new Color("#333333"), new Color("#cccccc")); } } } function drawDepartureRow(colStack, d, isLast) { const rowStack = colStack.addStack(); rowStack.layoutHorizontally(); rowStack.centerAlignContent(); drawTimeStack(rowStack, d); rowStack.addSpacer(4); drawInfoStack(rowStack, d); if (!isLast) { colStack.addSpacer(6); } } function drawColumn(parentStack, deps) { const colStack = parentStack.addStack(); colStack.layoutVertically(); if (!deps || deps.length === 0) { const emptyText = colStack.addText("Keine anstehenden Fahrten."); emptyText.font = Font.systemFont(10); emptyText.textColor = Color.dynamic(new Color("#666666"), new Color("#aaaaaa")); return; } const maxRows = rowCount; const depsToShow = deps.slice(0, maxRows); for (let i = 0; i < depsToShow.length; i++) { drawDepartureRow(colStack, depsToShow[i], i === depsToShow.length - 1); } } async function createWidget() { const widget = new ListWidget(); widget.backgroundColor = Color.dynamic(new Color("#ffffff"), new Color("#1a1a2e")); const data = await fetchDepartures(); if (data.error) { drawError(widget, data.error); return widget; } const mainStack = widget.addStack(); mainStack.layoutHorizontally(); if (isSmall) { if (widgetMode === "temp") { drawColumn(mainStack, data.temporary); } else { drawColumn(mainStack, data.permanent); } } else { drawColumn(mainStack, data.temporary); mainStack.addSpacer(12); drawColumn(mainStack, data.permanent); } return widget; } const widget = await createWidget(); if (config.runsInWidget) { Script.setWidget(widget); } else { // Test view in app if (isSmall) { widget.presentSmall(); } else if (isMedium) { widget.presentMedium(); } else { widget.presentLarge(); } } Script.complete();