Handball iOS Widgets mit Scriptable
🤾🏼‍♀️

Handball iOS Widgets mit Scriptable

💡
This site is only available in German.
In diesem Tutorial möchten wir zwei Widgets für das iPhone einbinden, um Informationen über unsere Handballmannschaft(en) zu erhalten. Dazu verwenden wir die App Scriptable, die es uns ermöglicht, eigenen JavaScript-Code einzubinden.
Das finale Ergebnis könnte wie folgt aussehen:
notion image

Voraussetzungen

Bitte schau dass du die folgenden Voraussetzungen erfüllst:
  • Du besitzt ein iPhone mit mindestens iOS 14 (Widgets müssen funktionieren).
  • Du hast eine Handball-Mannschaft, bei welcher du auf dem Laufenden bleiben willst.

(Optional) Handball-Mannschaft Identifier herausfinden

Zunächst benötigen wir den Identifikator, der der Handballmannschaft zugeordnet ist, deren Informationen wir in den Widgets anzeigen möchten. Dieser Schritt kann übersprungen werden, wenn der Identifikator bereits bekannt ist.
In der Regel folgt er dem Format "Wort.Wort.Zahl".

Besuche zunächst die Webseite handball.net.
Klicke auf das Bild um es zu Vergrößern.
Klicke auf das Bild um es zu Vergrößern.

Im oberen rechten Bildschirmeck befindet sich ein Suchfeld. Trage dort den Namen der Mannschaft ein, für die du den Identifikator herausfinden möchtest.
notion image
In diesem Beispiel suche ich nach dem Radebeuler Handball-Verein e.V. Bitte ersetze dieses Team durch das Team, das du suchst. Es ist wichtig zu erwähnen, dass ich zufällig auf diese Mannschaft gestoßen bin und dies nur als Beispiel verwende - ich stehe in keiner Verbindung zu diesem Team.

Nun öffnet sich eine Liste mit Mannschaften aus diesem Verband. Suche weiter nach der genauen Mannschaft, die du auswählen möchtest. Klicke dann auf die entsprechende Mannschaft.
notion image

Besorge dir einen Stift und Papier - der Identifikator ist quasi vor deinen Augen. Schau in die Adresszeile deines Browsers, wie auf dem Bild angegeben. Du suchst nach zwei Wörtern und einer Zahl, die mit "." verbunden sind. Orientiere dich bitte an dem Bild unten, um dich zu orientieren, behalte jedoch im Kopf, dass dein Identifikator sich stark unterscheiden wird.
notion image
Im obigen Beispiel wäre der Identifier “nuliga.hvs.1491908”.

Installieren des Tabellen-Codes

notion image
Um den Code für das Tabellen-Widget zu installieren, folge zunächst diesen Schritten:

Während du auf dem iPhone bist, kopiere den folgenden JavaScript-Code komplett:
(Du kannst den gesamten Code auf einmal kopieren, indem du im oberen rechten Eck auf das Kopiersymbol klickst. Wird im Folgenden kein JavaScript-Code angezeigt, lade die Seite neu.)
/* * * MIT License * * Copyright (c) 2024 Mathias Bauer * * wwww.mathiasbauer.com * info at mathiasbauer dot com * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ // // Schritt 1 von 1: // Setzen Sie den Identifier ihres Teams. // Wenn Sie den Identifier nicht wissen, besuchen Sie die folgende Website um ihn herauszufinden: // https://mathiasbauer.com/handball-ios-widgets-mit-scriptable // const teamIdentifier = "x"; // Font sizes const regularFontSize = 10; const refreshDateSize = 8; const headerTextSize = 10; const imageSize = 15; // Parsing widget parameters const params = args.widgetParameter ? args.widgetParameter.split(",") : []; // Constructing URL for API call const url = `https://www.handball.net/a/sportdata/1/widgets/team/${teamIdentifier}/table`; // Checking if dark theme is selected const isDarkTheme = params?.[0] === "dark"; // Creating a new ListWidget const widget = new ListWidget(); // Stack for header content const headerStack = widget.addStack(); // Variable to store home team name let homeTeamName; // Function to adjust widget settings function adjustSettings() { widget.url = "https://mathiasbauer.com/handball-ios-widgets-mit-scriptable"; if (isDarkTheme) { widget.backgroundColor = new Color("#1C1C1E"); // Dark theme background color } headerStack.setPadding(0, 0, 5, 0); } // Function to fetch data from the API async function fetchData(url) { const req = new Request(url); const apiResult = await req.loadJSON(); return { data: apiResult }; } // Function to load team logo image async function loadTeamLogo(logoPath) { logoPath = logoPath == null ? "https://www.handball.net/img/handball-net-logo-sm.png" : logoPath.replace("handball-net:", "https://www.handball.net/"); try { const req = new Request(logoPath); return await req.loadImage(); } catch (error) { console.error("Error loading team logo:", error); } } // Function to build the widget async function buildWidget() { adjustSettings(); const data = await fetchData(url); if (!data.data.table) { displayNoGameData(); return; } homeTeamName = data.data.teamSummary.name; let teamName = data.data.teamSummary.name ? data.data.table.teamSummary.name : "Unknown Team"; let headerText = headerStack.addText( homeTeamName + " " + teamName + " Spielplan" ); headerText.font = Font.boldSystemFont(headerTextSize); if (isDarkTheme) { headerText.textColor = new Color("#FFFFFF"); // White text color for dark theme } addTeam("Pl.", null, "Team", "Sp.", "Tore", "Pkt."); widget.addSpacer(1); for (const item of data.data.table.rows) { let place = item.rank; let image = await loadTeamLogo(item.team.logo); let name = item.team.name; let gamesPlayed = item.games; let points = item.points; let goals = item.goals; addTeam(place, image, name, gamesPlayed, points, goals); } widget.addSpacer(2); let refreshedAt = getRefreshedAtDate(); addRefreshedAtDate(refreshedAt); } // Function to get the refreshed at date function getRefreshedAtDate() { const date = new Date(); const day = date.getDate().toString().padStart(2, "0"); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); const formattedDate = `Aktualisiert am ${day}.${month} um ${hours}:${minutes} Uhr`; return formattedDate; } // Function to display message when no game data is available function displayNoGameData() { const noDataText = widget.addText("Keine aktuellen Spieldaten gefunden."); noDataText.font = Font.mediumSystemFont(regularFontSize); if (isDarkTheme) { noDataText.textColor = new Color("#FFFFFF"); // White text color for dark theme } } // Helper function to create a stack const createStack = (rowStack) => { const stack = rowStack.addStack(); stack.setPadding(0, 0, 0, 0); return stack; }; // Helper function to add text to a stack const addTextToStack = (stack, text, size = 0) => { const textItem = stack.addText(text.toString()); textItem.font = Font.mediumSystemFont(regularFontSize); stack.size = new Size(size, 0); textItem.leftAlignText(); return textItem; }; // Helper function to add an image to a stack const addImageToStack = (stack, image, size) => { if (image) { const imageNode = stack.addImage(image); imageNode.imageSize = new Size(size, size); imageNode.leftAlignImage(); stack.setPadding(0, 0, 0, size); } else { addTextToStack(stack, "", size + 15); } }; // Function to add a team row to the widget function addTeam(place, image, teamName, gamesPlayed, goals, points) { const rowStack = widget.addStack(); rowStack.setPadding(0, 0, 0, 0); rowStack.layoutHorizontally(); const placeStack = createStack(rowStack); const placeText = addTextToStack(placeStack, place, 15); const imageStack = createStack(rowStack); addImageToStack(imageStack, image, imageSize); const gamesPlayedStack = createStack(rowStack); const gamesPlayedText = addTextToStack(gamesPlayedStack, gamesPlayed, 20); const goalsStack = createStack(rowStack); const goalsText = addTextToStack(goalsStack, goals, 35); goalsText.rightAlignText(); const pointsStack = createStack(rowStack); const pointsText = addTextToStack(pointsStack, points, 35); const teamNameStack = createStack(rowStack); const teamNameText = addTextToStack(teamNameStack, teamName); teamNameText.leftAlignText(); if (isDarkTheme) { let darkThemeColour = new Color("#FFFFFF"); // White text color for dark theme placeText.textColor = darkThemeColour; gamesPlayedText.textColor = darkThemeColour; goalsText.textColor = darkThemeColour; pointsText.textColor = darkThemeColour; } if (teamName === homeTeamName) { let highlightColour = new Color("#4B9CD3"); // Highlight color for home team teamNameText.textColor = highlightColour; placeText.textColor = highlightColour; gamesPlayedText.textColor = highlightColour; goalsText.textColor = highlightColour; pointsText.textColor = highlightColour; } } // Function to add the refreshed at date to the widget function addRefreshedAtDate(date) { const dateStack = widget.addStack(); const dateStackName = dateStack.addText(date); dateStackName.font = Font.mediumSystemFont(refreshDateSize); } // Call the buildWidget function to start building the widget await buildWidget(); // Set the widget and complete the script Script.setWidget(widget); Script.complete(); // Present the widget in medium size widget.presentMedium();

Öffne die Scriptable App auf deinem iPhone

Klicke im oberen rechten Bildschirmeck auf das Plus-Zeichen. Eine neues Fenster öffnet sich. Füge dort den gerade kopierten JavaScript-Code ein.
notion image

Ersetze nun das "x" in Zeile 35 mit dem von dir vorhin herausgesuchten Identifikator deiner Handballmannschaft. Du musst weiterhin Anführungszeichen um den Identifikator haben. In diesem Beispiel kopiere ich für die Radebeuler Mannschaft den folgenden Identifikator (mit Anführungszeichen): "nuliga.hvs.1491908" und ersetze das "x" mit diesem Identifikator.
notion image

Das war's schon fast. Klicke im unteren rechten Eck auf das Play-Symbol, und die App startet.
notion image

Als Widget einrichten

An sich funktioniert die App. Jedoch möchten wir ja ein Widget. Halte auf dem Homescreen lange "ins Nichts", klicke oben rechts auf das Plus-Zeichen, um ein Widget hinzuzufügen.
notion image

Klicke länger auf das Widget, um es zu konfigurieren. Klicke im aufkommenden Menü auf “Widget bearbeiten”.
notion image

Suche in der Suchleiste nach Scriptable und bestätige.
notion image

Nun wählst du durch Wischen eine passende Größe aus. Ich empfehle mindestens die mittlere Größe zu wählen. Aber das musst du ausprobieren - je nachdem, wie viel bei deiner Mannschaft los ist. Klicke dann auf "Widget hinzufügen".
notion image

🥳🥳🥳 Fertig. Klopf dir auf die Schulter. Theoretisch funktioniert das Widget jetzt.

Installieren des Spielplan-Codes

notion image
Um das Spielplan-Widget zu installieren, kannst du analog zum Tabellen-Widget vorgehen. Kopiere den Code, binde ihn in Scriptable ein, passe den Identifikator an und binde das Widget mit dem Scriptable-Code ein.
/* * * MIT License * * Copyright (c) 2024 Mathias Bauer * * wwww.mathiasbauer.com * info at mathiasbauer dot com * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ // // Schritt 1 von 1: // Setzen Sie den den Identifier ihres Teams. // Wenn Sie den Identifier nicht wissen, besuchen Sie die folgende Website um ihn herauszufinden: // https://mathiasbauer.com/handball-ios-widgets-mit-scriptable // const teamIdentifier = "x"; // Font sizes const regularFontSize = 13; const refreshDateSize = 10; const headerTextSize = 13; const imageSize = 20; const params = args.widgetParameter ? args.widgetParameter.split(",") : []; const url = `https://www.handball.net/a/sportdata/1/widgets/team/${teamIdentifier}/team-schedule`; const isDarkTheme = params?.[0] === "dark"; const padding = 4; const widget = new ListWidget(); const headerStack = widget.addStack(); function adjustSettings() { widget.url = "https://mathiasbauer.com/handball-ios-widgets-mit-scriptable"; if (isDarkTheme) { widget.backgroundColor = new Color("#1C1C1E"); } widget.setPadding(padding, padding, padding, padding); headerStack.setPadding(0, 0, 8, 0); } async function fetchData(url) { const req = new Request(url); const apiResult = await req.loadJSON(); return { data: apiResult }; } async function loadTeamLogo(logoPath) { logoPath = logoPath == null ? "https://www.handball.net/img/handball-net-logo-sm.png" : logoPath.replace("handball-net:", "https://www.handball.net/"); try { const req = new Request(logoPath); return await req.loadImage(); } catch (error) { console.error("Error loading team logo:", error); } } async function buildWidget() { adjustSettings(); const data = await fetchData(url); let teamName = data.data.schedule.data[0].tournament.name ? data.data.schedule.data[0].tournament.name : "Unknown Team"; let headerText = headerStack.addText( data.data.team.name + " " + teamName + " Spielplan" ); headerText.font = Font.boldSystemFont(headerTextSize); if (isDarkTheme) { headerText.textColor = new Color("#FFFFFF"); } for (const item of data.data.schedule.data) { if (!item) { displayNoGameData(); break; } const homeTeamName = item.homeTeam.name ? item.homeTeam.name : ""; const awayTeamName = item.awayTeam.name ? item.awayTeam.name : ""; const homeTeamGoal = item.homeGoals ? item.homeGoals.toString() : "-"; const awayTeamGoal = item.awayGoals ? item.awayGoals.toString() : "-"; const gameStart = item.startsAt ? formatUnixTimestamp(item.startsAt) : ""; const place = item.field.name ? item.field.name : ""; const city = item.field.city ? item.field.city : ""; const gameInfo = gameStart && place && city ? `${gameStart} bei ${place} in ${city}` : ""; // Load team logos const homeTeamImage = await loadTeamLogo(item.homeTeam.logo); const awayTeamImage = await loadTeamLogo(item.awayTeam.logo); addTeam( homeTeamImage, homeTeamName, homeTeamGoal, homeTeamGoal > awayTeamGoal, awayTeamImage, awayTeamName, awayTeamGoal, awayTeamGoal > homeTeamGoal, gameInfo ); widget.addSpacer(5); } widget.addSpacer(1); let refreshedAt = getRefreshedAtDate(); addRefreshedAtDate(refreshedAt); } const formatUnixTimestamp = (timestamp) => { const date = new Date(timestamp); const daysOfWeek = [ "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", ]; const months = [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember", ]; const hours = ("0" + date.getHours()).slice(-2); const minutes = ("0" + date.getMinutes()).slice(-2); const dayOfWeek = daysOfWeek[date.getDay()]; const dayOfMonth = ("0" + date.getDate()).slice(-2); const month = months[date.getMonth()]; return `${hours}:${minutes} Uhr am ${dayOfWeek}, den ${dayOfMonth}. ${month}`; }; function getRefreshedAtDate() { const date = new Date(); const day = date.getDate().toString().padStart(2, "0"); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); const formattedDate = `Aktualisiert am ${day}.${month} um ${hours}:${minutes} Uhr`; return formattedDate; } function displayNoGameData() { const noDataText = widget.addText("Keine weiteren Spieldaten gefunden"); noDataText.font = Font.mediumSystemFont(regularFontSize); if (isDarkTheme) { noDataText.textColor = new Color("#FFFFFF"); } } function createRowStack() { const rowStack = widget.addStack(); rowStack.layoutHorizontally(); return rowStack; } function createStack(parentStack) { const stack = parentStack.addStack(); return stack; } function addGameInfo(stack, info) { const gameInfoText = stack.addText(info); gameInfoText.font = Font.mediumSystemFont(regularFontSize); gameInfoText.leftAlignText(); } function setImage(stack, image) { stack.setPadding(0, 0, 0, 10); const imageNode = stack.addImage(image); imageNode.imageSize = new Size(imageSize, imageSize); imageNode.leftAlignImage(); } function setTeamName(stack, name) { stack.setPadding(0, 8, 0, 8); const teamNameText = stack.addText(name); teamNameText.font = Font.mediumSystemFont(regularFontSize); } function setGoals(stack, goals, isScoreBigger) { let possibleGoals = goals === "null" ? "-" : goals; const goalsText = stack.addText(possibleGoals); goalsText.font = Font.mediumSystemFont(regularFontSize); goalsText.rightAlignText(); if (goals === "-") { if (isDarkTheme) { goalsText.textColor = new Color("#000000"); } else { goalsText.textColor = new Color("#FFFFFF"); } return; } goalsText.textColor = isScoreBigger ? new Color("#4AA956") : new Color("#D22E2E"); } function addTeam( homeImage, homeTeamName, homeGoals, isHomeScoreBigger, awayImage, awayTeamName, awayGoals, isAwayScoreBigger, gameInfo ) { const homeRowStack = createRowStack(); const awayRowStack = createRowStack(); const gameInfoStack = createRowStack(); // Adding home and away team information const homeGameInfoStack = createStack(gameInfoStack); const homeImageStack = createStack(homeRowStack); const awayImageStack = createStack(awayRowStack); const homeGoalsStack = createStack(homeRowStack); const awayGoalsStack = createStack(awayRowStack); const homeTeamNameStack = createStack(homeRowStack); const awayTeamNameStack = createStack(awayRowStack); addGameInfo(homeGameInfoStack, gameInfo); setImage(awayImageStack, awayImage); setImage(homeImageStack, homeImage); setGoals(homeGoalsStack, homeGoals, isHomeScoreBigger); setGoals(awayGoalsStack, awayGoals, isAwayScoreBigger); setTeamName(homeTeamNameStack, homeTeamName); setTeamName(awayTeamNameStack, awayTeamName); widget.addSpacer(4); } function addRefreshedAtDate(date) { const dateStack = widget.addStack(); const dateStackName = dateStack.addText(date); dateStackName.font = Font.mediumSystemFont(refreshDateSize); } await buildWidget(); Script.setWidget(widget); Script.complete(); widget.presentSmall();

Bonus: Eigene Anpassungen

Wir haben inhaltlich gar nicht über den Code gesprochen. Was macht er eigentlich? Schau gerne mal in den Quelltext rein. Da dieses hier kein Coding-Tutorial sein soll, lasse ich dich jetzt ins kalte Wasser fallen.
Wenn du Bock hast:
Teile den Code mit FreundenInnen (MIT-Lizenz)
Teile diesen Artikel
Nimm optische Anpassungen vor
Schriftgröße
Text-/Hintergrundfarben
Platz zwischen Elementen
Lies dir die Scriptable Dokumentation durch
Verbesser meinen ranzigen Code nach deinem Gutdünken