Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save Leibinger015/596cf1cdaf5cfd8477dfeff5219c07ab to your computer and use it in GitHub Desktop.

Select an option

Save Leibinger015/596cf1cdaf5cfd8477dfeff5219c07ab to your computer and use it in GitHub Desktop.
Scriptable Widget Script for iPhone and iPad
// 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