Created
July 30, 2026 10:25
-
-
Save Leibinger015/596cf1cdaf5cfd8477dfeff5219c07ab to your computer and use it in GitHub Desktop.
Scriptable Widget Script for iPhone and iPad
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Variables used by Scriptable. | |
| // Script original: https://github.com/yannikmotzet/Lidl-Connect-iOS-Widget | |
| // Edited version by: www.anb030.de | |
| // Edit-Date: 04.07.2026 | |
| // New Special feature: 3 Widget Colors and Design Update | |
| // ===== Login-Daten anpassen ===== | |
| username = '0152xxxxxxxx' | |
| password = 'yourLIDLpassword' | |
| apiUrl = 'https://api.lidl-connect.de/api' | |
| // ===== Darstellung festlegen ===== | |
| // 'auto' = folgt Systemeinstellung | |
| // 'light' = immer helles Design | |
| // 'dark' = immer dunkles Design | |
| // 'lidl' = LIDL-Markenfarben | |
| appearanceMode = 'dark' | |
| fresh = false | |
| fm = FileManager.local() | |
| path = fm.joinPath(fm.documentsDirectory(), "lidl_connect.json") | |
| let lidlConnectWidget = await createWidget() | |
| if (!config.runsInWidget) { | |
| await lidlConnectWidget.presentSmall() | |
| } | |
| Script.setWidget(lidlConnectWidget) | |
| Script.complete() | |
| // get access token | |
| async function getToken() { | |
| let req = new Request(apiUrl + '/token') | |
| req.method = 'POST'; | |
| req.headers = { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'application/json' | |
| } | |
| req.body = JSON.stringify({ | |
| 'client_id': 'lidl', | |
| 'client_secret': 'lidl', | |
| 'grant_type': 'password', | |
| 'password': password, | |
| 'username': username | |
| }) | |
| try { | |
| let token = await req.loadJSON() | |
| return token['access_token'] | |
| } catch (e) { | |
| console.log('Login failed!') | |
| } | |
| } | |
| // request data from server | |
| async function getConsumptions() { | |
| let token = await getToken() | |
| let req = new Request(apiUrl + '/graphql') | |
| req.method = 'POST'; | |
| req.headers = { | |
| 'Content-Type': 'application/json', | |
| 'Accept': 'application/json', | |
| 'Authorization': 'Bearer ' + token | |
| } | |
| req.body = JSON.stringify({ | |
| 'operationName': 'consumptions', | |
| 'query': 'query consumptions {\n consumptions {\n consumptionsForUnit {\n consumed\n unit\n formattedUnit\n type\n description\n expirationDate\n left\n max\n tariffOrOptions {\n name\n id\n type\n consumptions {\n consumed\n unit\n formattedUnit\n type\n description\n expirationDate\n left\n max\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', | |
| 'variables': {} | |
| }) | |
| try { | |
| data = await req.loadJSON() | |
| if (data['data']['consumptions']['consumptionsForUnit'].length > 0) { | |
| extractConsumptions(data) | |
| } else { | |
| let consumption = {"booked": false} | |
| fm.writeString(path, JSON.stringify(consumption, null, 2)) | |
| } | |
| fresh = true | |
| } catch (e) { | |
| data = JSON.parse(fm.readString(path), null) | |
| if (!data) { | |
| console.log('Request failed!') | |
| } | |
| } | |
| } | |
| // extract consumption from response json | |
| async function extractConsumptions(data) { | |
| let consumption = {} | |
| consumption['booked'] = true | |
| consumption['consumed'] = data['data']['consumptions']['consumptionsForUnit'][0]['consumed'] | |
| consumption['max'] = data['data']['consumptions']['consumptionsForUnit'][0]['max'] | |
| consumption['percentage'] = Math.round(consumption['consumed'] / consumption['max'] * 100) | |
| consumption['unit'] = data['data']['consumptions']['consumptionsForUnit'][0]['unit'] | |
| consumption['expirationDate'] = data['data']['consumptions']['consumptionsForUnit'][0]['expirationDate'] | |
| // add 24 hours of last day | |
| let timeDiff = Date.parse(consumption['expirationDate']) + (1000*60*60*24) - Date.parse(new Date()) | |
| consumption['daysRemaining'] = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) | |
| consumption['hoursRemaining'] = Math.floor((timeDiff / (1000 * 60 * 60)) % 24) | |
| fm.writeString(path, JSON.stringify(consumption, null, 2)) | |
| } | |
| // ===== Ring-Grafik zeichnen ===== | |
| // zeichnet einen Fortschrittsring als Bild inkl. zentriertem Prozentwert | |
| function drawRing(percentage, ringColor, textColor, trackColor, greyedOutColor, greyedOut) { | |
| const size = 220 | |
| const lineWidth = 18 | |
| const radius = size / 2 - lineWidth / 2 - 4 | |
| const ctx = new DrawContext() | |
| ctx.size = new Size(size, size) | |
| ctx.opaque = false | |
| ctx.respectScreenScale = true | |
| const center = new Point(size / 2, size / 2) | |
| const trackRect = new Rect(center.x - radius, center.y - radius, radius * 2, radius * 2) | |
| // Hintergrund-Track (leerer Ring) | |
| ctx.setStrokeColor(trackColor) | |
| ctx.setLineWidth(lineWidth) | |
| ctx.strokeEllipse(trackRect) | |
| // Fortschritts-Bogen | |
| if (percentage > 0) { | |
| ctx.setStrokeColor(greyedOut ? greyedOutColor : ringColor) | |
| ctx.setLineWidth(lineWidth) | |
| const startAngle = -Math.PI / 2 // 12 Uhr Position | |
| const clampedPct = Math.min(percentage, 100) | |
| const endAngle = startAngle + (clampedPct / 100) * 2 * Math.PI | |
| const steps = Math.max(2, Math.round(180 * (clampedPct / 100))) | |
| const points = [] | |
| for (let i = 0; i <= steps; i++) { | |
| const angle = startAngle + (endAngle - startAngle) * (i / steps) | |
| points.push(new Point( | |
| center.x + radius * Math.cos(angle), | |
| center.y + radius * Math.sin(angle) | |
| )) | |
| } | |
| const path = new Path() | |
| path.addLines(points) | |
| ctx.addPath(path) | |
| ctx.strokePath() | |
| } | |
| // Prozent-Text zentriert in den Ring (jetzt größer) | |
| ctx.setTextAlignedCenter() | |
| ctx.setTextColor(greyedOut ? greyedOutColor : textColor) | |
| ctx.setFont(Font.boldSystemFont(56)) | |
| const textRect = new Rect(0, size / 2 - 36, size, 72) | |
| ctx.drawTextInRect(percentage.toString() + '%', textRect) | |
| return ctx.getImage() | |
| } | |
| async function createWidget() { | |
| await getConsumptions() | |
| data = JSON.parse(fm.readString(path), null) | |
| const widget = new ListWidget() | |
| // Aktuellen Erscheinungsmodus bestimmen. | |
| // Color.dynamic() wird von DrawContext (gezeichnetes Ring-Bild) nicht zuverlässig | |
| // unterstützt, daher werden hier feste Farben passend zum gewählten Modus verwendet. | |
| let isDark | |
| if (appearanceMode === 'dark') { | |
| isDark = true | |
| } else if (appearanceMode === 'light') { | |
| isDark = false | |
| } else if (appearanceMode === 'lidl') { | |
| isDark = false // ungenutzt, Theme überschreibt Farben komplett | |
| } else { | |
| isDark = Device.isUsingDarkAppearance() | |
| } | |
| let bgColor, primaryTextColor, titleColor, greyColor, trackColor, fixedRingColor | |
| if (appearanceMode === 'lidl') { | |
| // LIDL-Markenfarben: Blau als Hintergrund, Gelb für Titel & Werte, Rot nur für den Ring | |
| bgColor = new Color("#0050aa") | |
| primaryTextColor = new Color("#fff000") | |
| titleColor = new Color("#fff000") | |
| greyColor = new Color("#fff000", 0.75) | |
| trackColor = new Color("#e60a14", 0.3) | |
| fixedRingColor = new Color("#e60a14") | |
| } else { | |
| bgColor = isDark ? new Color("#000000") : new Color("#ffffff") | |
| primaryTextColor = isDark ? Color.white() : Color.black() | |
| titleColor = primaryTextColor | |
| greyColor = isDark ? new Color("#8e8e93") : new Color("#6e6e73") | |
| trackColor = isDark ? new Color("#3a3a3c") : new Color("#d1d1d6") | |
| fixedRingColor = null // Ampelfarbe wird unten je nach Verbrauch bestimmt | |
| } | |
| widget.backgroundColor = bgColor | |
| if (!data) { | |
| let line1 = widget.addText("Für die initiale Ausführung wird eine Internetverbindung benötigt.") | |
| line1.font = Font.mediumSystemFont(14) | |
| line1.textColor = primaryTextColor | |
| } else if (!data['booked']) { | |
| let line1 = widget.addText("Aktuell ist kein Datentarif gebucht.") | |
| line1.font = Font.mediumSystemFont(14) | |
| line1.textColor = primaryTextColor | |
| } else { | |
| try { | |
| let usedPercentage = data['percentage'] | |
| // Farbe je nach Verbrauch (außer im LIDL-Theme: dort immer fest Rot) | |
| let ringColor | |
| if (fixedRingColor) { | |
| ringColor = fixedRingColor | |
| } else { | |
| ringColor = Color.green() | |
| if (usedPercentage >= 90) { | |
| ringColor = Color.red() | |
| } else if (usedPercentage >= 75) { | |
| ringColor = Color.orange() | |
| } | |
| } | |
| // Titel | |
| widget.addSpacer(4) | |
| const title = widget.addText('LIDL Connect') | |
| title.font = Font.boldSystemFont(13) | |
| title.textColor = titleColor | |
| title.centerAlignText() | |
| // Ring zentriert | |
| widget.addSpacer(4) | |
| const ringImage = drawRing(usedPercentage, ringColor, primaryTextColor, trackColor, greyColor, !fresh) | |
| const ringStack = widget.addStack() | |
| ringStack.addSpacer() | |
| const imgElement = ringStack.addImage(ringImage) | |
| imgElement.imageSize = new Size(78, 78) | |
| ringStack.addSpacer() | |
| // GB Anzeige darunter | |
| widget.addSpacer(6) | |
| const gbText = widget.addText(data['consumed'].toString() + ' ' + data['unit'] + ' / ' + data['max'].toString() + ' ' + data['unit']) | |
| gbText.font = Font.mediumSystemFont(12) | |
| gbText.textColor = fresh ? primaryTextColor : greyColor | |
| gbText.centerAlignText() | |
| // verbleibende Zeit darunter | |
| widget.addSpacer(3) | |
| let timeRemainingString = '' | |
| if (data['daysRemaining'] > 1) { | |
| timeRemainingString = (data['daysRemaining'] + 1).toString() + ' Tage übrig' | |
| } else { | |
| if (data['daysRemaining'] == 1) { | |
| timeRemainingString = '1 Tag & ' | |
| } | |
| if (data['hoursRemaining'] == 1) { | |
| timeRemainingString += '1 Stunde übrig' | |
| } else { | |
| timeRemainingString += data['hoursRemaining'].toString() + ' Stunden übrig' | |
| } | |
| } | |
| const timeText = widget.addText(timeRemainingString) | |
| timeText.font = Font.mediumSystemFont(12) | |
| timeText.textColor = fresh ? primaryTextColor : greyColor | |
| timeText.centerAlignText() | |
| } catch (err) { | |
| let errText = widget.addText('Error fetching JSON') | |
| errText.textColor = primaryTextColor | |
| console.log(err) | |
| } | |
| } | |
| // Zeit + Datum der letzten Aktualisierung, z.B. "19:10 • 03.07.2026" | |
| widget.addSpacer(6) | |
| let now = new Date() | |
| let dateFormatter = new DateFormatter() | |
| dateFormatter.dateFormat = "HH:mm '•' dd.MM.yyyy" | |
| let timeLabel = widget.addText(dateFormatter.string(now)) | |
| timeLabel.font = Font.mediumSystemFont(10) | |
| timeLabel.centerAlignText() | |
| timeLabel.textColor = greyColor | |
| return widget | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment