Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
iOS Widget, das die Anzahl an Klopapier Packungen in deiner nächsten dm Drogerie anzeigt (für die scriptable.app)
// dm Klopapier Widget
//
// Copyright (C) 2020 by marco79 <marco79cgn@gmail.com>
//
// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
// OF THIS SOFTWARE.
//
// Toilet paper icon made by boettges
let country = 'de' // für Österreich bitte 'at' verwenden
let storeId = 251
let param = args.widgetParameter
if (param != null && param.length > 0) {
storeId = param
}
const widget = new ListWidget()
const storeInfo = await fetchStoreInformation()
const storeCapacity = await fetchAmountOfPaper()
await createWidget()
// used for debugging if script runs inside the app
if (!config.runsInWidget) {
await widget.presentSmall()
}
Script.setWidget(widget)
Script.complete()
// build the content of the widget
async function createWidget() {
widget.addSpacer(4)
const logoImg = await getImage('dm-logo.png')
widget.setPadding(10, 10, 10, 10)
const titleFontSize = 12
const detailFontSize = 36
const logoStack = widget.addStack()
logoStack.addSpacer(86)
const logoImageStack = logoStack.addStack()
logoStack.layoutHorizontally()
logoImageStack.backgroundColor = new Color("#ffffff", 1.0)
logoImageStack.cornerRadius = 8
const wimg = logoImageStack.addImage(logoImg)
wimg.imageSize = new Size(40, 40)
wimg.rightAlignImage()
widget.addSpacer()
const icon = await getImage('toilet-paper.png')
let row = widget.addStack()
row.layoutHorizontally()
row.addSpacer(2)
const iconImg = row.addImage(icon)
iconImg.imageSize = new Size(40, 40)
row.addSpacer(13)
let column = row.addStack()
column.layoutVertically()
const paperText = column.addText("KLOPAPIER")
paperText.font = Font.mediumRoundedSystemFont(13)
const packageCount = column.addText(storeCapacity.toString())
packageCount.font = Font.mediumRoundedSystemFont(22)
if (storeCapacity < 30) {
packageCount.textColor = new Color("#E50000")
} else {
packageCount.textColor = new Color("#00CD66")
}
widget.addSpacer(4)
const row2 = widget.addStack()
row2.layoutVertically()
const street = row2.addText(storeInfo.address.street)
street.font = Font.regularSystemFont(11)
const zipCity = row2.addText(storeInfo.address.zip + " " + storeInfo.address.city)
zipCity.font = Font.regularSystemFont(11)
let currentTime = new Date().toLocaleTimeString('de-DE', { hour: "numeric", minute: "numeric" })
let currentDay = new Date().getDay()
let isOpen
if (currentDay > 0) {
const todaysOpeningHour = storeInfo.openingHours[currentDay-1].timeRanges[0].opening
const todaysClosingHour = storeInfo.openingHours[currentDay-1].timeRanges[0].closing
const range = [todaysOpeningHour, todaysClosingHour];
isOpen = isInRange(currentTime, range)
} else {
isOpen = false
}
let shopStateText
if (isOpen) {
shopStateText = row2.addText('Geöffnet')
shopStateText.textColor = new Color("#00CD66")
} else {
shopStateText = row2.addText('Geschlossen')
shopStateText.textColor = new Color("#E50000")
}
shopStateText.font = Font.mediumSystemFont(11)
}
// fetches the amount of toilet paper packages
async function fetchAmountOfPaper() {
let url
let counter = 0
if (country.toLowerCase() === 'at') {
// Austria
const array = ["156754", "180487", "194066", "188494", "194144", "273259", "170237", "232201", "170425", "283216", "205873", "205874", "249881", "184204"]
for (var i = 0; i < array.length; i++) {
let currentItem = array[i]
url = 'https://products.dm.de/store-availability/AT/products/dans/' + currentItem + '/stocklevel?storeNumbers=' + storeId
let req = new Request(url)
let apiResult = await req.loadJSON()
if (req.response.statusCode == 200) {
counter += apiResult.storeAvailability[0].stockLevel
}
}
} else {
// Germany
url = 'https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=' + storeId
const req = new Request(url)
const apiResult = await req.loadJSON()
for (var i in apiResult.storeAvailabilities) {
counter += apiResult.storeAvailabilities[i][0].stockLevel
}
}
return counter
}
// fetches information of the configured store, e.g. opening hours, address etc.
async function fetchStoreInformation() {
let url
if (country.toLowerCase() === 'at') {
url = 'https://store-data-service.services.dmtech.com/stores/item/at/' + storeId
widget.url = 'https://www.dm.at/search?query=toilettenpapier&searchType=product'
} else {
url = 'https://store-data-service.services.dmtech.com/stores/item/de/' + storeId
widget.url = 'https://www.dm.de/search?query=toilettenpapier&searchType=product'
}
let req = new Request(url)
let apiResult = await req.loadJSON()
return apiResult
}
// checks whether the store is currently open or closed
function isInRange(value, range) {
return value >= range[0] && value <= range[1];
}
// get images from local filestore or download them once
async function getImage(image) {
let fm = FileManager.local()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir, image)
if (fm.fileExists(path)) {
return fm.readImage(path)
} else {
// download once
let imageUrl
switch (image) {
case 'dm-logo.png':
imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Dm_Logo.svg/300px-Dm_Logo.svg.png"
break
case 'toilet-paper.png':
imageUrl = "https://i.imgur.com/Uv1qZGV.png"
break
default:
console.log(`Sorry, couldn't find ${image}.`);
}
let iconImage = await loadImage(imageUrl)
fm.writeImage(path, iconImage)
return iconImage
}
}
// helper function to download an image from a given url
async function loadImage(imgUrl) {
const req = new Request(imgUrl)
return await req.loadImage()
}
// end of script
// bitte bis zum Ende kopieren
@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 21, 2020

Intro

Das dm Klopapier Widget zeigt die Vorräte an Toilettenpapier in deiner nächsten dm Drogerie. Die einzig zuverlässige #Klopapiergarantie.

Bekannt aus dem Netz: RTL, Macwelt, Business Punk, Berliner Zeitung, iPhone-ticker, t3n, Caschys Blog, RND, Giga, Computer Bild, Chip, Curved, Notebooksbilliger, Vowe, Billiger-Telefonieren u.a.

Die Store ID für deine gewünschte dm Drogerie kann in den Einstellungen des Widgets als Parameter konfiguriert werden. Dadurch können auch mehrere Widgets parallel auf dem Homescreen eingerichtet werden für unterschiedliche Geschäfte.

Anforderungen

Um deine Store ID zu ermitteln:

  • Öffne die dm Shop Finder Seite
  • Gib deine Postleitzahl ein und wähle den gewünschten Markt aus
  • Klick dann auf "Weitere Details"
    Die store id steht dann in der Browser URL, zum Beispiel "https://www.dm.de/store/de-2449/koeln/hohenzollernring-58" ist die store id 2449. Diese Nummer kannst du in den Einstellungen des Widgets auf dem Homescreen unter "Parameter" eintragen.

Installation

  • Kopiere den Source code von oben (klick vorher auf "raw" oben rechts)
  • Öffne die Scriptable app
  • Klick auf das "+" Symbol oben rechts und füge das kopierte Skript ein
  • Klick auf den Titel des Skripts ganz oben und vergebe einen Namen (z.B. DM-Klopapier)
  • Speichere das Skript durch Klick auf "Done" oben links
  • Gehe auf deinen iOS Homescreen und drücke irgendwo lang, um in den "wiggle mode" zu kommen (mit dem man auch die App Symbole anordnen kann)
  • Drücke das "+" Symbol oben links, blättere dann nach unten zu "Scriptable" (Liste ist alphabetisch), wähle die erste Widget Größe (small) und drück unten auf "Widget hinzufügen"
  • Drücke auf das Widget, um seine Einstellungen zu bearbeiten (optional lang drücken, wenn der Wiggle Modus schon beendet wurde)
  • Wähle unter "Script" das oben erstellte aus (DM-Klopapier)
  • Gib als "Parameter" die ermittelten Store ID ein, z.B. 180 für Mainz

Danke

Großer Dank an @simonbs für großartige Apps wie Scriptable, DataJar oder Jayson.

Disclaimer

Es handelt sich um ein von mir selbst entwickeltes Spaßprojekt, kein offizielles Produkt der dm Drogerie. Ich stehe in keinerlei Beziehung zu dm und bekomme weder Provision noch kostenloses Klopapier.

Updates

24.10.2020, 00:24

  • Bugfix → Öffnungszeitenberechnung (off-by-one, d'oh!)
    WICHTIG: Bitte aktualisiert auf die neueste Version, damit das Skript auch an Samstagen und Sonntagen funktioniert. Sorry!

23.10.2020, 17:49

  • Fix für Österreich (da store ids nicht eindeutig sind zwischen Deutschland und Österreich)
    → bitte oben das Land (country) auf 'at' setzen für Österreich

23.10.2020, 00:05

  • Unterstützung für Österreich
  • Layout auf 4,7" iPhones optimiert (6S/7/8/SE2)
  • Icons werden jetzt nur einmal geladen und gecachet

22.10.2020, 18:35

  • Fehler bei der Berechnung der Öffnungszeiten gefixt
  • neues Klopapier icon: es hängt jetzt richtig rum! (Danke für die zahlreichen Hinweise)
  • dark mode Verbesserungen

22.10.2020, 18:53

  • automatische dark mode Erkennung entfernt, da sie im widget Modus nicht zuverlässig funktioniert
@Sillium

This comment has been minimized.

Copy link

@Sillium Sillium commented Oct 22, 2020

Sehr schöne Idee, sehr schön umgesetzt! 👏

@oschni

This comment has been minimized.

Copy link

@oschni oschni commented Oct 22, 2020

Sehr geil!

Vielleicht noch wie im Inzidenzen Script für die Dark Mode Nutzer folgendes einbauen.

if(Device.isUsingDarkAppearance()) { street.textColor = Color.white() zipCity.textColor = Color.white() } else { street.textColor = Color.black() zipCity.textColor = Color.black() }

@colinferm

This comment has been minimized.

Copy link

@colinferm colinferm commented Oct 22, 2020

Ich finde das sehr geil! Jetzt installiert

@klprint

This comment has been minimized.

Copy link

@klprint klprint commented Oct 22, 2020

Wirklich tolle Idee mit Liebe zum Detail 👍

@osxdoc

This comment has been minimized.

Copy link

@osxdoc osxdoc commented Oct 22, 2020

Tolles script, wie oschni schon schreibt wäre die Anpassung an den darkmode wünschenswert. Unser Dm wir um 10:30 Uhr als geschlossen angezeigt. ID 1589

@ckienast

This comment has been minimized.

Copy link

@ckienast ckienast commented Oct 22, 2020

Hier ne Version inklusive dark mode fix.

@heikokrebs

This comment has been minimized.

Copy link

@heikokrebs heikokrebs commented Oct 22, 2020

Großartig! Vielen lieben Dank!

@felix-wolf

This comment has been minimized.

Copy link

@felix-wolf felix-wolf commented Oct 22, 2020

Wirklich cool! Gibt es für die DM-Tech API eine Doku oder hast du dir die Request zusammengesucht?

@kennyx986

This comment has been minimized.

Copy link

@kennyx986 kennyx986 commented Oct 22, 2020

Wie komme ich an die DM ID? 🤷‍♂️

@oschni

This comment has been minimized.

Copy link

@oschni oschni commented Oct 22, 2020

Wie komme ich an die DM ID? 🤷‍♂️

Ist oben in den Requirements beschrieben!

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Hier ne Version inklusive dark mode fix.

Danke, habe ich oben so ergänzt. Nutze tatsächlich nie den Dark Mode. Ich schau mal, ob ich dafür ein helleres dm Logo finde.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Wirklich cool! Gibt es für die DM-Tech API eine Doku oder hast du dir die Request zusammengesucht?

Ich habe mir das über die Developer Tools im Browser hergeleitet. Der Shop macht das gleiche, wenn du dort surfst/kaufst. Die IDs der Produkte habe ich über eine Suche bekommen.
https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=177

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Tolles script, wie oschni schon schreibt wäre die Anpassung an den darkmode wünschenswert. Unser Dm wir um 10:30 Uhr als geschlossen angezeigt. ID 1589

Sowohl der kleine Bug bezüglich der Öffnungszeiten als auch der Dark Mode sind inzwischen korrigiert. Einfach nochmal neu kopieren. :)

@bigbang11

This comment has been minimized.

Copy link

@bigbang11 bigbang11 commented Oct 22, 2020

Sehr schön, aber leider werden alle Artikel die unter "Toilettenpapier" im Suchergebnis bei DM gelistet sind aufgeführt, also auch feuchtes Toilettenpapier, was ja nicht die Mangelware ist sondern das normale Klopapier. Kannst du da noch was optimieren? Ansonsten tolles und witziges Widget

@kevinkub

This comment has been minimized.

Copy link

@kevinkub kevinkub commented Oct 22, 2020

Hey @marco79cgn - super coole Idee! Ich habe das Inzidenz Widget gebaut.
Mein letzter Kenntnisstand zum Darkmode ist der, dass du den Darkmode im Widget nicht abfragen kannst (steht auch in der Scriptable Doku). Um Dark-Mode richtig zu implementieren kann man aktuell nur mit den Defaults arbeiten (indem man keine Farben angibt). Die Standard Textfarbe ist dann schwarz/weiß und die Hintergrundfarbe ist weiß/schwarz für Light/Dark-Mode. Du ersparst dir viele Tweets, wenn du das berücksichtigst ;-).

Edit: Und Glückwunsch zum nächsten Social-Media-Hype :-).

@DSchumacher2104

This comment has been minimized.

Copy link

@DSchumacher2104 DSchumacher2104 commented Oct 22, 2020

Super Idee! Leider kann ich nicht (gut genug) programmieren, kann das eventuell jemand für die Auslastungsanzeige von FitX Sportstudios umsetzen? Z. B. https://www.fitx.de/fitnessstudios/gelsenkirchen-hessler

Wäre super!

@loveallsurfall

This comment has been minimized.

Copy link

@loveallsurfall loveallsurfall commented Oct 22, 2020

Ich habe mir das über die Developer Tools im Browser hergeleitet. Der Shop macht das gleiche, wenn du dort surfst/kaufst. Die IDs der Produkte habe ich über eine Suche bekommen.
https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=177

wie kommt man an die IDs ... hab es nicht herausgefunden?!?

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Sehr schön, aber leider werden alle Artikel die unter "Toilettenpapier" im Suchergebnis bei DM gelistet sind aufgeführt, also auch feuchtes Toilettenpapier, was ja nicht die Mangelware ist sondern das normale Klopapier. Kannst du da noch was optimieren? Ansonsten tolles und witziges Widget

Das Widget selbst beachtet keine feuchten Toilettenpapiere etc. Ich habe extra alle IDs der korrekten Produkte rausgesucht. Nur wenn du den Link öffnest, landest du auf der dm Seite und dort sind dann auch die ganzen Feuchttücher etc.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Hey @marco79cgn - super coole Idee! Ich habe das Inzidenz Widget gebaut.
Mein letzter Kenntnisstand zum Darkmode ist der, dass du den Darkmode im Widget nicht abfragen kannst (steht auch in der Scriptable Doku). Um Dark-Mode richtig zu implementieren kann man aktuell nur mit den Defaults arbeiten (indem man keine Farben angibt). Die Standard Textfarbe ist dann schwarz/weiß und die Hintergrundfarbe ist weiß/schwarz für Light/Dark-Mode. Du ersparst dir viele Tweets, wenn du das berücksichtigst ;-).

Edit: Und Glückwunsch zum nächsten Social-Media-Hype :-).

Danke! :)
Ja, das ist korrekt. Die Methode, die eigentlich zurück geben soll, ob der Benutzer gerade Darkmode aktiviert hat oder nicht, funktioniert nicht zuverlässig. Ich versuche eine universelle Lösung zu basteln, die in beiden Fällen gut aussieht.
Ebenfalls Glückwunsch zu deinem Inzidenz-Widget. :)

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Ich habe mir das über die Developer Tools im Browser hergeleitet. Der Shop macht das gleiche, wenn du dort surfst/kaufst. Die IDs der Produkte habe ich über eine Suche bekommen.
https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=177

wie kommt man an die IDs ... hab es nicht herausgefunden?!?

Im Browser die Developer Tools öffnen, dort den Netzwerk Tab und dort dann schauen was aufgerufen wird, wenn du z.B. im Shop auf "Toilettenpapier" klickst. Dieser Call hier liefert dann alle Ergebnisse:
https://products.dm.de/product/de/search?productQuery=%3Arelevance%3AallCategories%3A060201&hideFacets=true&hideSorts=true&pageSize=60

@dsturm

This comment has been minimized.

Copy link

@dsturm dsturm commented Oct 22, 2020

Chapeau 👏🏼 Sehr gute Umsetzung und (leider) sehr geile Idee! 😀

@ghost

This comment has been minimized.

Copy link

@ghost ghost commented Oct 22, 2020

Mich köstlich amüsiert. Aber schon auch etwas traurig, dass man Egoismus damit auch noch unterstützt. Dennoch echt witzige Idee. 😊

@vgr28

This comment has been minimized.

Copy link

@vgr28 vgr28 commented Oct 22, 2020

Kannst du dir mal Markt 1279 anschauen? Da fehlen bei mir einige Informationen....

@joergrech

This comment has been minimized.

Copy link

@joergrech joergrech commented Oct 22, 2020

Bei mir wird im Widget nur der Scriptable Hintergrund (Play Symbol mit verschiedenen Icons) angezeigt - das Ausführen in Scriptable funktioniert und zeigt auch das Widget an. Das Widget ist auf den Script-Namen, "Run Script" und den Parameter der DM Filiale gesetzt. Muss man die Ausführung von Widgets in iOS oder Scriptable freischalten?

@fischerphilipp

This comment has been minimized.

Copy link

@fischerphilipp fischerphilipp commented Oct 22, 2020

Super Idee und Umsetzung!
Eine Frage hab ich hier allerdings noch und evtl. ist das eher eine Frage in Richtung Scriptable:
Wie oft aktualisiert das Widget? Wird das durch den iOS Scheduler übernommen der die Widgets automatisch aktualisiert und man dementsprechend keinen Einfluss drauf hat?

@steffenrohwer

This comment has been minimized.

Copy link

@steffenrohwer steffenrohwer commented Oct 22, 2020

Hammer gutes Projekt und sehr witzig 😂! Wie genau bist du bei einzelnen Produkten auf die IDs gekommen? Dann könnte man dich ja eine Version basteln, wo zb nur das lieblingspapier angezeigt wird

@monza258

This comment has been minimized.

Copy link

@monza258 monza258 commented Oct 22, 2020

Mega geil. Bin mal gespannt wo die Reise mit solchen Widgets hingeht. Lässt sich das auch für andere Artikel bzw andere Geschäfte realisieren? ALDI, LIDL?

Push Meldung wenn ein Artikel ausverkauft war und wieder verfügbar sein sollte wäre noch interessant.

Bin gespannt auf weiter Ideen uns Scripts von dir @marco79cgn
Es wird die Community sehr freuen wenn hier noch weitere Scripts folgen

@colinferm

This comment has been minimized.

Copy link

@colinferm colinferm commented Oct 22, 2020

Ich habe meinen Geschäft im Widget immer geschlossen gesehen. Aber hier ist meine Lösung.

let today = new Date()
let currentDay = today.getDay()
const todaysOpeningHour = storeInfo.openingHours[currentDay].timeRanges[0].opening.substring(0,2)
const todaysClosingHour = storeInfo.openingHours[currentDay].timeRanges[0].closing.substring(0,2)
const range = [new Date().setHours(todaysOpeningHour), new Date().setHours(todaysClosingHour)]
const isOpen = isInRange(today, range)

Jetzt möchte ich etwas für Budni...

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Bei mir wird im Widget nur der Scriptable Hintergrund (Play Symbol mit verschiedenen Icons) angezeigt - das Ausführen in Scriptable funktioniert und zeigt auch das Widget an. Das Widget ist auf den Script-Namen, "Run Script" und den Parameter der DM Filiale gesetzt. Muss man die Ausführung von Widgets in iOS oder Scriptable freischalten?

Das sollte eigentlich direkt funktionieren. Es gibt leider ein paar Bugs, die mit iOS 14.1 wieder gekommen sind. Im Zweifel mal rebooten, das Widget komplett löschen und von vorne beginnen.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Hammer gutes Projekt und sehr witzig 😂! Wie genau bist du bei einzelnen Produkten auf die IDs gekommen? Dann könnte man dich ja eine Version basteln, wo zb nur das lieblingspapier angezeigt wird

Ich habe analysiert, was im Browser passiert, wenn man dort shoppt. Schau mal hier. Der Call liefert alle Produkte mit IDs und Beschreibung. Daraus habe ich die IDs gefiltert, die mich interessieren (also kein Feuchtpapier zum Beispiel).
https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2#gistcomment-3499658

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Mega geil. Bin mal gespannt wo die Reise mit solchen Widgets hingeht. Lässt sich das auch für andere Artikel bzw andere Geschäfte realisieren? ALDI, LIDL?

Push Meldung wenn ein Artikel ausverkauft war und wieder verfügbar sein sollte wäre noch interessant.

Bin gespannt auf weiter Ideen uns Scripts von dir @marco79cgn
Es wird die Community sehr freuen wenn hier noch weitere Scripts folgen

Danke! Bei ALDI oder LIDL ist mir nicht bekannt, dass sie eine Auskunft haben über ihr Sortiment. Für andere Produkte von dm ginge das natürlich.

@steffenrohwer

This comment has been minimized.

Copy link

@steffenrohwer steffenrohwer commented Oct 22, 2020

Hammer gutes Projekt und sehr witzig 😂! Wie genau bist du bei einzelnen Produkten auf die IDs gekommen? Dann könnte man dich ja eine Version basteln, wo zb nur das lieblingspapier angezeigt wird

Ich habe analysiert, was im Browser passiert, wenn man dort shoppt. Schau mal hier. Der Call liefert alle Produkte mit IDs und Beschreibung. Daraus habe ich die IDs gefiltert, die mich interessieren (also kein Feuchtpapier zum Beispiel).
https://gist.github.com/marco79cgn/23ce08fd8711ee893a3be12d4543f2d2#gistcomment-3499658

Da werden mir dann aber keine namen sondern nur Verfügbarkeiten von bestimmten Nummern angezeigt, wenn ich den Link öffne...

@rialoe

This comment has been minimized.

Copy link

@rialoe rialoe commented Oct 22, 2020

Ziemlich cool, funktioniert auch reibungslos! Allerdings schaff ich es nicht, dasselbe für österreichische Filialen hinzubekommen (wohne in Wien, da macht das Sinn). Die store-IDs sind leicht zu bekommen (bei store-ID 170 komm' ich z.B. oft vorbei), was sonst noch zu änderen wäre, ist mir ein Rätsel. Ich bin dankbar über jeden Tipp :)

@mountbatt

This comment has been minimized.

Copy link

@mountbatt mountbatt commented Oct 22, 2020

Für iPhone 7 passt KLOPAPIER nicht mehr ganz in das Widget.
Daher besser Fontsize 13 statt 14

paperText.font = Font.mediumRoundedSystemFont(13)

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Ziemlich cool, funktioniert auch reibungslos! Allerdings schaff ich es nicht, dasselbe für österreichische Filialen hinzubekommen (wohne in Wien, da macht das Sinn). Die store-IDs sind leicht zu bekommen (bei store-ID 170 komm' ich z.B. oft vorbei), was sonst noch zu änderen wäre, ist mir ein Rätsel. Ich bin dankbar über jeden Tipp :)

Update: Österreich wird jetzt auch unterstützt. Ist etwas aufwendiger, da man jeden Artikel einzeln aufrufen muss. Aber klappt!

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 22, 2020

Für iPhone 7 passt KLOPAPIER nicht mehr ganz in das Widget.
Daher besser Fontsize 13 statt 14

paperText.font = Font.mediumRoundedSystemFont(13)

Danke für das Feedback, ist jetzt angepasst.

@veraverto

This comment has been minimized.

Copy link

@veraverto veraverto commented Oct 22, 2020

Tolles Skript, vielen Dank dafür! Habe zwar kein Interesse an Klopapier, dafür aber an Mehl und mir das Skript dafür umgebaut. Ich hoffe ja aber immer noch auf die Vernunft der Menschen und dass ich diese Spielerei nicht ernsthaft brauche.

@achisto

This comment has been minimized.

Copy link

@achisto achisto commented Oct 22, 2020

Tolles Skript, vielen Dank dafür! Habe zwar kein Interesse an Klopapier, dafür aber an Mehl und mir das Skript dafür umgebaut. Ich hoffe ja aber immer noch auf die Vernunft der Menschen und dass ich diese Spielerei nicht ernsthaft brauche.>

Mich interessierte Mehl auch mehr als Klopapier, habe den Code als Fork online gestellt: https://gist.github.com/achisto/ebccd305226218f1cbd01540213403c3

5693F4FB-3CF7-40BE-B38A-55AB2ACF9826

@quintus-luis

This comment has been minimized.

Copy link

@quintus-luis quintus-luis commented Oct 23, 2020

Geniale Idee!
Was würdet ihr davon halten daraus wirklich ne iOS App zu bauen? Das wird mit den AppStore Guidlines zwar schwer, ist aber machbar. Und das ist für viele Nutzer mit Sicherheit genauso lustig, wie für uns!

Vielleicht mit verschiedenen Produkten und Store Suche. Wär ein cooles Projekt denke ich und auch super gefragt außerhalb der Tech-Bubble.

@xMaNuu

This comment has been minimized.

Copy link

@xMaNuu xMaNuu commented Oct 23, 2020

@achisto du hast die Hefe vergessen ;-)

@Aim23

This comment has been minimized.

Copy link

@Aim23 Aim23 commented Oct 23, 2020

Hi Marco,
herrliche Idee die du hast! Ich hab mich gleich ran gesetzt und eine Adaption in Python und Flask geschrieben.

Unter:
https://products.dm.de/product/de/search?productQuery=%3Arelevance%3AallCategories%3A060201&hideFacets=false&hideSorts=false&pageSize=100 findet man alle Artikel in dieser Rubrik. Marco hat ja bereits die Feuchttücher aussortiert 👍

Ich habe für mich erstmal alle Artikel drin belassen und per Abfrage nachgeschaut ob diese Verfügbar sind.

Alle dan's habe ich dann in eine Liste geschrieben und per
https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=1945
abgefragt.

Über eine weitere Abfrage dann alle "inStock": false aussortiert und dann die Summe über das "stockLevel": <amount> gebildet.

Für mich das erste mal das ich in Flask gearbeitet habe. Die Flask App kann dann z.B. auf einem Raspberry Pi, AWS oder anderswo online bzw. lokal laufen.

Da dein Script auf Caschys Blog ein wenig eingeschlagen hat und die Leute nach einer Android Lösung gesucht haben, habe ich die Lösung mittels Flask gewählt. Über Tasker lässt sich dann einfach ein HTTP GET Request ausführen, der dann weiter verarbeitet werden kann.

Beispiel Bilder sind hier https://imgur.com/a/qgqjyxD zu finden.

@masselmello

This comment has been minimized.

Copy link

@masselmello masselmello commented Oct 23, 2020

Super Idee! Leider kann ich nicht (gut genug) programmieren, kann das eventuell jemand für die Auslastungsanzeige von FitX Sportstudios umsetzen? Z. B. https://www.fitx.de/fitnessstudios/gelsenkirchen-hessler

Wäre super!

@DSchumacher2104 Ich schaus mir Mal an. Hab schon eins fürs McFit geschrieben ;)
https://gist.github.com/masselmello/6d4f4c533b98b2550ee23a7a5e6c6cff

@masselmello

This comment has been minimized.

Copy link

@masselmello masselmello commented Oct 23, 2020

Und an Marco, danke für das super Widget! :D Endlich Mal was, um meinen hässlichen Homescreen mit bissl Klopapier aufzuhübschen. ^^

@DSchumacher2104

This comment has been minimized.

Copy link

@DSchumacher2104 DSchumacher2104 commented Oct 23, 2020

@DSchumacher2104 Ich schaus mir Mal an. Hab schon eins fürs McFit geschrieben ;)
https://gist.github.com/masselmello/6d4f4c533b98b2550ee23a7a5e6c6cff

@masselmello Cool, leider bin ich dort kein Mitglied 🙄. Könntest du das auch für FitX programmieren 🙏🏽✊️🍀😊

@boettges

This comment has been minimized.

Copy link

@boettges boettges commented Oct 23, 2020

Super Idee! Leider kann ich nicht (gut genug) programmieren, kann das eventuell jemand für die Auslastungsanzeige von FitX Sportstudios umsetzen? Z. B. https://www.fitx.de/fitnessstudios/gelsenkirchen-hessler

Wäre super!

Schau dir den Gist mal an: (vorerst entfernt)
Stand schon eine Weile auf meiner to-do Liste. 😉

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 23, 2020

Geniale Idee!
Was würdet ihr davon halten daraus wirklich ne iOS App zu bauen? Das wird mit den AppStore Guidlines zwar schwer, ist aber machbar. Und das ist für viele Nutzer mit Sicherheit genauso lustig, wie für uns!

Vielleicht mit verschiedenen Produkten und Store Suche. Wär ein cooles Projekt denke ich und auch super gefragt außerhalb der Tech-Bubble.

Keine schlechte Idee, aber das ist eigentlich die Aufgabe der dm Entwicklungsabteilung. Sie nahmen dieses widget hier mit Humor (lucky me), aber eine App ginge sicherlich etwas zu weit. Zumal sie die api auch problemlos absichern könnten, so dass man nicht mehr ohne weiteres darauf zugreifen könnte. Abgesehen davon gibt der Online-Shop sowie deren eigene App diese Funktion ja schon mehr oder weniger her. Also eine Bestandsanzeige für Artikel in einem beliebigen Shop.

Und eigentlich war das alles als reiner Gag gedacht. Aber der Monk in mir wollte es eben funktional haben, nicht mit fake Daten. Freut mich natürlich, was das für ein Feedback gab!

Vielen Dank an dieser Stelle an alle für die (größtenteils) positiven Reaktionen und natürlich für die ganzen Stars hier bei Github. 👍

Leider kann ich mangels Android Smartphone keine Portierung machen. Mir wäre auch keine Android App bekannt, mit der man auf solch einfache Weise content auf den Homescreen bringen kann. Scriptable ist diesbezüglich schlicht genial.

@achisto

This comment has been minimized.

Copy link

@achisto achisto commented Oct 23, 2020

Leider kann ich mangels Android Smartphone keine Portierung machen. Mir wäre auch keine Android App bekannt, mit der man auf solch einfache Weise content auf den Homescreen bringen kann. Scriptable ist diesbezüglich schlicht genial.

Beim Inzidenz-Widget hatte ein User das bereits für Android portiert. Hier der Link zum Fork. Vielleicht auch hier möglich? Habe leider auch keinen Androiden daheim, sonst würde ich das testen.

@eopo

This comment has been minimized.

Copy link

@eopo eopo commented Oct 23, 2020

Da offensichtlich Bedarf besteht:
FitnessFirst: https://gist.github.com/eopo/9344584035f487db0e229d655bdb39c4
FitX: https://gist.github.com/eopo/aedaf03f1f27a0c9c02f4974f3f4c9ba
McFit, John Reed, JOHN & JANE's, High5: https://gist.github.com/eopo/b0df56edbfc0df816adbf4a54047adbf
XTRAFit: https://gist.github.com/eopo/f0dc692f23e2373000df6134b7b4e4e0
Alle anderen größeren Ketten stellen entweder keine Auslastungsanzeige zur Verfügung oder deren Websites sind so mies programmiert, dass mir der Aufwand zu hoch ist.
@DSchumacher2104 ;)

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 23, 2020

Sneak Preview 2.0: Gute Idee oder zu viel des Guten?
Einige fragten auch zusätzlich noch nach Hefe. @achisto hatte ja auch schon einen Fork nur mit Mehl. Hmm...

@JDTm

This comment has been minimized.

Copy link

@JDTm JDTm commented Oct 23, 2020

Alle anderen größeren Ketten stellen entweder keine Auslastungsanzeige zur Verfügung oder deren Websites sind so mies programmiert, dass mir der Aufwand zu hoch ist.
@DSchumacher2104 ;)

Cool! Wie ist das mit Fitstar? https://www.fit-star.de/

@JDTm

This comment has been minimized.

Copy link

@JDTm JDTm commented Oct 23, 2020

Ich bekomme grad eine Fehlermeldunfg: Error on Line 78:65: TypeError: undefined is not an obejct (evaluating 'storeInfo.openinghours[currentDay]timeRanges').
Eine Alarmierung wenn eine neue Version bereit steht, wäre auch cool.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 23, 2020

Ich bekomme grad eine Fehlermeldunfg: Error on Line 78:65: TypeError: undefined is not an obejct (evaluating 'storeInfo.openinghours[currentDay]timeRanges').
Eine Alarmierung wenn eine neue Version bereit steht, wäre auch cool.

Sorry, da war ein fieser Bug drin, den ich bereits behoben habe. Bitte das Skript nochmal neu kopieren.

Ganz oben im ersten Kommentar steht immer, wenn es Updates gibt. Vielleicht überführe ich das mal in ein Github Projekt mit ReadMe.

@JDTm

This comment has been minimized.

Copy link

@JDTm JDTm commented Oct 24, 2020

Sorry, da war ein fieser Bug drin, den ich bereits behoben habe. Bitte das Skript nochmal neu kopieren.

Ganz oben im ersten Kommentar steht immer, wenn es Updates gibt. Vielleicht überführe ich das mal in ein Github Projekt mit ReadMe.

Ich dachte eher im Script: Script ruft zusätzlich noch Webseite X auf und prüft ob es eine neue Version gibt und zeigt dann eine rote Ecke im Widget?

@cru7aa

This comment has been minimized.

Copy link

@cru7aa cru7aa commented Oct 24, 2020

Error on line 164:40: ReferenceError: Can't find variable: loadImage

iOS 14.1
Scriptable 1.5.3

8298CA74-3B8A-432E-9470-A01AA9CC5598

@mountbatt

This comment has been minimized.

Copy link

@mountbatt mountbatt commented Oct 24, 2020

Error on line 164:40: ReferenceError: Can't find variable: loadImage

iOS 14.1
Scriptable 1.5.3

8298CA74-3B8A-432E-9470-A01AA9CC5598

Du hast nicht alles kopiert!

Vielleicht würde es helfen, wenn man am Ende des RAW Codes ein paar Leerzeilen hat. Das würde das Kopieren in iOS vereinfachen!

@klaus-schuster

This comment has been minimized.

Copy link

@klaus-schuster klaus-schuster commented Oct 24, 2020

vielen dank! bin begeistert!

@cru7aa

This comment has been minimized.

Copy link

@cru7aa cru7aa commented Oct 24, 2020

Error on line 164:40: ReferenceError: Can't find variable: loadImage
iOS 14.1
Scriptable 1.5.3
8298CA74-3B8A-432E-9470-A01AA9CC5598

Du hast nicht alles kopiert!

Vielleicht würde es helfen, wenn man am Ende des RAW Codes ein paar Leerzeilen hat. Das würde das Kopieren in iOS vereinfachen!

Unglaublich… da kopiert man es 3x damit genau DAS nicht passiert, und dann passiert es doch…
Kaum macht man es richtig, schon funktioniert es!
Danke

@RealDeemer

This comment has been minimized.

Copy link

@RealDeemer RealDeemer commented Oct 24, 2020

Ich möchte anregen, den query von “toilettenpapier” zu “toilettenpapier%20rolle” anzupassen. Das knappe gut sind ja die Rollen und nicht die feuchten Waschlappen.

@veraverto

This comment has been minimized.

Copy link

@veraverto veraverto commented Oct 24, 2020

Sneak Preview 2.0: Gute Idee oder zu viel des Guten?
Einige fragten auch zusätzlich noch nach Hefe. @achisto hatte ja auch schon einen Fork nur mit Mehl. Hmm...

Ich würde es großartig finden, wenn ich mir die verschiedenen Mehlsorten und -typen im
Skript aussuchen könnte. Also beispielsweise Weizenmehl Type 550, 1050, Vollkorn und Roggenmehl Vollkorn. Habe es schon selbst probiert, aber leider sind meine Programmierkenntnisse dafür zu gering.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

Du hast nicht alles kopiert!

Vielleicht würde es helfen, wenn man am Ende des RAW Codes ein paar Leerzeilen hat. Das würde das Kopieren in iOS vereinfachen!

Gute Idee, das ist schon mehreren passiert. Ich habe noch eine Leerzeile und einen Kommentar eingefügt am Ende. Wenn man den nicht kopiert, passiert zumindest kein Fehler. :)

@DSchumacher2104

This comment has been minimized.

Copy link

@DSchumacher2104 DSchumacher2104 commented Oct 24, 2020

Da offensichtlich Bedarf besteht:
FitnessFirst: https://gist.github.com/eopo/9344584035f487db0e229d655bdb39c4
FitX: https://gist.github.com/eopo/aedaf03f1f27a0c9c02f4974f3f4c9ba
McFit, John Reed, JOHN & JANE's, High5: https://gist.github.com/eopo/b0df56edbfc0df816adbf4a54047adbf
Alle anderen größeren Ketten stellen entweder keine Auslastungsanzeige zur Verfügung oder deren Websites sind so mies programmiert, dass mir der Aufwand zu hoch ist.
@DSchumacher2104 ;)

@eopo Super, vielen Dank👍😊

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

Ich möchte anregen, den query von “toilettenpapier” zu “toilettenpapier%20rolle” anzupassen. Das knappe gut sind ja die Rollen und nicht die feuchten Waschlappen.

Ich habe nur die Artikelnummern (dans) von "echten" Klopapierrollen rausgesucht. Feuchttücher werden bei der Anzahl im Widget nicht berücksichtigt. Lediglich wenn man drauf tippt und dann in den dm Shop geleitet wird, werden alle Produkte angezeigt.

@DSchumacher2104

This comment has been minimized.

Copy link

@DSchumacher2104 DSchumacher2104 commented Oct 24, 2020

Sneak Preview 2.0: Gute Idee oder zu viel des Guten?
Einige fragten auch zusätzlich noch nach Hefe. @achisto hatte ja auch schon einen Fork nur mit Mehl. Hmm...

@marco79cgn Ich finde die Idee für V2.0 super, allerdings wäre es tatsächlich optimal, wenn man auch einzelne Mehlsorten auswählen könnte👍😊

@DSchumacher2104

This comment has been minimized.

Copy link

@DSchumacher2104 DSchumacher2104 commented Oct 24, 2020

Schau dir den Gist mal an: (vorerst entfernt)
Stand schon eine Weile auf meiner to-do Liste. 😉

@boettges Warum hast du dein Script rausgenommen?

@cru7aa

This comment has been minimized.

Copy link

@cru7aa cru7aa commented Oct 24, 2020

Zum Thema Mehl:
Generell finde ich das gut. Aber Mehl ist spezieller als Klopapier, wobei auch da manche das super flauschige mit Einhornfell brauchen…
Als Kommentar in den Code könnte man die Mehlsorten (und Klopapier Typen) aufnehmen, damit der User sich sein personalisierten Query bauen kann - wobei das vermutlich viel cooy&paste Fleißarbeit bedeutet…?!

@endyman

This comment has been minimized.

Copy link

@endyman endyman commented Oct 24, 2020

Ein kleiner Vorschlag zu dem Fix für die index out of bounds exception die auch für verkaufsoffene Sonntage funktioniert:

if (currentDay <= storeInfo.openingHours.length) {...

wohingegen bei

if (currentDay > 0) {...

die dm-stores im widget Sonntags immer geschlossen bleiben. 🤓

@CKone01

This comment has been minimized.

Copy link

@CKone01 CKone01 commented Oct 24, 2020

könnte man das Ganze nicht etwas generischer machen und neben der ShopID auch die ProductID, den Query, den Produktname der angezeigt werden soll und die URL zum png als Parameter übergeben?

Ich hab mir das gestern auf Balea Flüssigseife umgebaut und heute musste ich die Änderungen alle neu da reinbasteln.

Sneak Preview 2.0 ist natürlich auch sehr geil :D

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

Ein kleiner Vorschlag zu dem Fix für die index out of bounds exception die auch für verkaufsoffene Sonntage funktioniert:

if (currentDay <= storeInfo.openingHours.length) {...

wohingegen bei

if (currentDay > 0) {...

die dm-stores im widget Sonntags immer geschlossen bleiben. 🤓

Viel besser, Danke! An verkaufsoffene Sonntage habe ich gar nicht gedacht. Werde ich so einbauen.

An dieser Stelle Sorry für meine überschaubaren Javascript skills. Da bin ich nicht wirklich zuhause. Echte Javascript geeks würden den Code sicher anders schreiben, mit lambdas, promises, fancy pancy etc. Meine Variablennamen sich sicher auch viel zu lang (→ mehr als ein Buchstabe)... ;)

@CKone01

This comment has been minimized.

Copy link

@CKone01 CKone01 commented Oct 24, 2020

Ich würde es großartig finden, wenn ich mir die verschiedenen Mehlsorten und -typen im
Skript aussuchen könnte. Also beispielsweise Weizenmehl Type 550, 1050, Vollkorn und Roggenmehl Vollkorn. Habe es schon selbst probiert, aber leider sind meine Programmierkenntnisse dafür zu gering.

na s schwierig ist das doch nicht die ProductIDs herauszufinden und in die Liste einzusetzen - hat mehr mit basteln als mit programmieren zu tun...
MicrosoftTeams-image

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

könnte man das Ganze nicht etwas generischer machen und neben der ShopID auch die ProductID, den Query, den Produktname der angezeigt werden soll und die URL zum png als Parameter übergeben?

Ich hab mir das gestern auf Balea Flüssigseife umgebaut und heute musste ich die Änderungen alle neu da reinbasteln.

Sneak Preview 2.0 ist natürlich auch sehr geil :D

Klar könnte man das. Aber dann wird es (m.M.n.) viel fehleranfälliger und "Otto-Normal-Verbraucher" wäre damit vermutlich ziemlich überfordert. Ich mag das Skript am liebsten so, dass man gar nichts editieren muss. Die Lösung für Österreich gefällt mir schon nicht wirklich, hatte ich erst anders geplant. Aber leider gibt es Überschneidungen bei den store ids, sie sind also nicht eindeutig (nur eindeutig pro Land).

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

Zum Thema Mehl:
Generell finde ich das gut. Aber Mehl ist spezieller als Klopapier, wobei auch da manche das super flauschige mit Einhornfell brauchen…
Als Kommentar in den Code könnte man die Mehlsorten (und Klopapier Typen) aufnehmen, damit der User sich sein personalisierten Query bauen kann - wobei das vermutlich viel cooy&paste Fleißarbeit bedeutet…?!

Für dich ist Mehl vielleicht spezieller. Andere haben auch bei Klopapier ihre ganz klaren Vorlieben (ich nutze z.B. ausschließlich Recycling-Papier).

Wenn es im Notfall hart auf hart kommt ist man sicherlich nicht mehr so wählerisch. Und darum ging es doch letztendlich. ;)

@CKone01

This comment has been minimized.

Copy link

@CKone01 CKone01 commented Oct 24, 2020

ok, wie willste das dann im Preview 2.0 machen wenn einer Mehl und der andere Hefe mag?

Ich würde nur Produkt 1,2,3 machen und alles von außen eingeben.

Andere Sache ist das ich zB hier 4 dm in unmittelbarer Nähe habe..... aber ich weiß schon das das exponentiell komplex wird so langsam ;)

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

ok, wie willste das dann im Preview 2.0 machen wenn einer Mehl und der andere Hefe mag?

Ich würde nur Produkt 1,2,3 machen und alles von außen eingeben.

Andere Sache ist das ich zB hier 4 dm in unmittelbarer Nähe habe..... aber ich weiß schon das das exponentiell komplex wird so langsam ;)

Zumindest letzteres wäre aktuell schon lösbar. Du müsstest vier widgets auf dem Homescreen platzieren und dann in den Einstellungen des Widgets die jeweilige Store ID als Parameter übergeben. Diese ID gewinnt dann gegenüber der im Skript konfigurierten:

@achisto

This comment has been minimized.

Copy link

@achisto achisto commented Oct 24, 2020

Ein Vorschlag, um die Land-Auswahl für Nutzer*innen einfacher zu machen: mehrere Parameter übergeben, einen davon als country, einen als storeID.

Im Code sieht das dann so aus:

Bildschirmfoto 2020-10-24 um 13 49 47

Und so im Widget-Editor:

photo_2020-10-24 13 52 17

Natürlich wäre auch hier die if-Abfrage möglich, ob überhaupt eine storeID übergeben wurde, habe ich der Einfachheit halber weg gelassen. Ich versuche gerade mit diesem Ansatz eine Lösung für die "Mehl-Auswahl" zu basteln, aber theoretisch ließe sich das ja erweitern, dass Nutzer*innen zwei Parameter übergeben, welche beiden Produkte sie gerne sehen würden, und dann anhand der Parameter die richtige dan-Liste im Script gewählt wird. Das wären dann vier Parameter
country = params[0]
storeId = params[1]
product1 = params[2]
product2 = params[3]

Nur ein Gedankenspiel, wird natürlich immer komplexer dadurch, vor allem durch die unterschiedlichen Anfragen an den AT- und DE-Store ...

Nachtrag: ein weiterer Vorteil, den ich ganz grundsätzlich sehe, wenn möglichst viele Anpassungen über Parameter laufen: Nutzer*innen müssen bei einem Script-Update nicht immer wieder Variablen im Code ändern, sondern lediglich das neue Script in den Widget-Settings auswählen.

Nachtrag 2: ich habe meinen Fork jetzt dahingehend erweitert, dass eine Auswahl der Mehlsorte möglich ist.

mehlv2

@veraverto

This comment has been minimized.

Copy link

@veraverto veraverto commented Oct 24, 2020

na s schwierig ist das doch nicht die ProductIDs herauszufinden und in die Liste einzusetzen - hat mehr mit basteln als mit programmieren zu tun...

@CKone01 Das habe ich, wie oben beschrieben, bereits gemacht. Ich habe es allerdings nicht geschafft, mehrere Artikel gleichzeitig abzufragen und darzustellen.

@FWeinb

This comment has been minimized.

Copy link

@FWeinb FWeinb commented Oct 24, 2020

Sehr schönes Widget. Ich hatte heute die Möglichkeit implementiert Widgets per JSX-like Syntax in Scriptable zu erstellen (auf basis von htm). Mehr infos hier oder hier auf Twitter

Da dieses Widget vom Layout recht Umfangreich ist habe ich es mal in JSX nachgebaut. Im Kern ist das der Code der zum erstellen des Widgets genutzt wird:

async function createWidget() {
    const logoImg = await getImage('dm-logo.png');
    const icon = await getImage('toilet-paper.png');

    let currentTime = new Date().toLocaleTimeString('de-DE', { hour: "numeric", minute: "numeric" })
    let currentDay = new Date().getDay()
    let isOpen
    if (currentDay > 0) {
        const todaysOpeningHour = storeInfo.openingHours[currentDay-1].timeRanges[0].opening
        const todaysClosingHour = storeInfo.openingHours[currentDay-1].timeRanges[0].closing
        const range = [todaysOpeningHour, todaysClosingHour];
        isOpen = isInRange(currentTime, range)
    } else {
        isOpen = false
    }

    return render`
      <ListWidget padding=${[10,10,10,10]}>
        <Spacer length=${4} />
        <HStack>
          <Spacer />
          <Stack backgroundColor=${Color.white()} cornerRadius=${8}>
            <Image image=${logoImg} size=${new Size(40, 40)} />
          </Stack>
        </HStack>
        <Spacer />
        <HStack>
          <Spacer length=${2} />
          <Image image=${icon} size=${new Size(40, 40)} />
          <Spacer length=${13} />
          <VStack>
            <Text font=${Font.mediumRoundedSystemFont(13)}>KLOPAPIER</Text>
            <Text 
              font=${Font.mediumRoundedSystemFont(22)} 
              color=${storeCapacity < 30 ?  new Color("#E50000") : new Color("#00CD66")}
              >${
                storeCapacity.toString()
              }
            </Text>
          </VStack>
        </HStack>
        <Spacer length=${4} />
        <VStack>
          <Text font=${Font.regularSystemFont(11)}>${storeInfo.address.street}</Text>
          <Text font=${Font.regularSystemFont(11)}>${storeInfo.address.zip + ' ' + storeInfo.address.city}</Text>
          ${isOpen 
            ? h`<Text font=${Font.mediumSystemFont(11)} color=${new Color("#00CD66")}>Geöffnet</Text>`
            : h`<Text font=${Font.mediumSystemFont(11)} color=${new Color("#E50000")}>Geschlossen</Text>`
          }
        </VStack>
      </ListWidget>
    `;
}

Da sich durch die Nutzung von JSX die Struktur etwas geändert hat (klare Trennung von Daten und Anzeige) musste ich leider auch die fetchStoreInformation() Funktion dahingehend ändern das die URL für das Widget zurückgeben wird anstatt direkt in das Widget geschrieben zu werden.

Hier der gesamter Quellcode

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: blue; icon-glyph: magic;


const country = 'de' // replace with 'at' for shops in Austria
const storeId = 251
const param = args.widgetParameter
if (param != null && param.length > 0) {
    storeId = param
}

const storeInfo = await fetchStoreInformation()
const storeCapacity = await fetchAmountOfPaper()
const widget = await createWidget()

// used for debugging if script runs inside the app
if (!config.runsInWidget) {
    await widget.presentSmall()
}
Script.setWidget(widget)
Script.complete()

// build the content of the widget
async function createWidget() {
    const logoImg = await getImage('dm-logo.png');
    const icon = await getImage('toilet-paper.png');

    let currentTime = new Date().toLocaleTimeString('de-DE', { hour: "numeric", minute: "numeric" })
    let currentDay = new Date().getDay()
    let isOpen
    if (currentDay > 0) {
        const todaysOpeningHour = storeInfo.openingHours[currentDay-1].timeRanges[0].opening
        const todaysClosingHour = storeInfo.openingHours[currentDay-1].timeRanges[0].closing
        const range = [todaysOpeningHour, todaysClosingHour];
        isOpen = isInRange(currentTime, range)
    } else {
        isOpen = false
    }

    return render`
      <ListWidget padding=${[10,10,10,10]}>
        <Spacer length=${4} />
        <HStack>
          <Spacer />
          <Stack backgroundColor=${Color.white()} cornerRadius=${8}>
            <Image image=${logoImg} size=${new Size(40, 40)} />
          </Stack>
        </HStack>
        <Spacer />
        <HStack>
          <Spacer length=${2} />
          <Image image=${icon} size=${new Size(40, 40)} />
          <Spacer length=${13} />
          <VStack>
            <Text font=${Font.mediumRoundedSystemFont(13)}>KLOPAPIER</Text>
            <Text 
              font=${Font.mediumRoundedSystemFont(22)} 
              color=${storeCapacity < 30 ?  new Color("#E50000") : new Color("#00CD66")}
              >${
                storeCapacity.toString()
              }
            </Text>
          </VStack>
        </HStack>
        <Spacer length=${4} />
        <VStack>
          <Text font=${Font.regularSystemFont(11)}>${storeInfo.address.street}</Text>
          <Text font=${Font.regularSystemFont(11)}>${storeInfo.address.zip + ' ' + storeInfo.address.city}</Text>
          ${isOpen 
            ? h`<Text font=${Font.mediumSystemFont(11)} color=${new Color("#00CD66")}>Geöffnet</Text>`
            : h`<Text font=${Font.mediumSystemFont(11)} color=${new Color("#E50000")}>Geschlossen</Text>`
          }
        </VStack>
      </ListWidget>
    `;
}

// fetches the amount of toilet paper packages
async function fetchAmountOfPaper() {
    let url
    let counter = 0
    if (country.toLowerCase() === 'at') {
        // Austria
        const array = ["156754", "180487", "194066", "188494", "194144", "273259", "170237", "232201", "170425", "283216", "205873", "205874", "249881", "184204"]
        for (var i = 0; i < array.length; i++) {
            let currentItem = array[i]
            url = 'https://products.dm.de/store-availability/AT/products/dans/' + currentItem + '/stocklevel?storeNumbers=' + storeId
            let req = new Request(url)
            let apiResult = await req.loadJSON()
            if (req.response.statusCode == 200) {
                counter += apiResult.storeAvailability[0].stockLevel
            }
        }
    } else {
        // Germany
        url = 'https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=' + storeId
        const req = new Request(url)
        const apiResult = await req.loadJSON()
        for (var i in apiResult.storeAvailabilities) {
            counter += apiResult.storeAvailabilities[i][0].stockLevel
        }
    }
    return counter
}

// fetches information of the configured store, e.g. opening hours, address etc.
async function fetchStoreInformation() {
    const url = `https://store-data-service.services.dmtech.com/stores/item/${country.toLocaleLowerCase()}/${storeId}`
    const widgetUrl = `https://www.dm.${country.toLocaleLowerCase()}/search?query=toilettenpapier&searchType=product`
    let req = new Request(url)
    let storeInfo = await req.loadJSON()
    return { widgetUrl, ...storeInfo }
}

// checks whether the store is currently open or closed
function isInRange(value, range) {
    return value >= range[0] && value <= range[1];
}

// get images from local filestore or download them once
async function getImage(image) {
    let fm = FileManager.local()
    let dir = fm.documentsDirectory()
    let path = fm.joinPath(dir, image)
    if (fm.fileExists(path)) {
        return fm.readImage(path)
    } else {
        // download once
        let imageUrl
        switch (image) {
            case 'dm-logo.png':
                imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/5/50/Dm_Logo.svg/300px-Dm_Logo.svg.png"
                break
            case 'toilet-paper.png':
                imageUrl = "https://i.imgur.com/Uv1qZGV.png"
                break
            default:
                console.log(`Sorry, couldn't find ${image}.`);
        }
        let iconImage = await loadImage(imageUrl)
        fm.writeImage(path, iconImage)
        return iconImage
    }
}

// helper function to download an image from a given url
async function loadImage(imgUrl) {
    const req = new Request(imgUrl)
    return await req.loadImage()
}

// Library Code
function render(...args) {
  const NativeElements = {
    ListWidget(ast) {
      return applyAttrs(new ListWidget(), ast.attrs);
    },
    Text(ast, parent) {
      const { attrs, children } = ast;
      parent = parent.addText(children.join(' '));
      return applyAttrs(parent, attrs, {
        color: 'textColor',
        opacity: 'textOpacity',
        center: 'centerAlignText',
        left: 'leftAlignText',
        right: 'rightAlignText',
      });
    },
    Stack(ast, parent) {
      parent = parent.addStack();
      return applyAttrs(parent, ast.attrs, {
        vertical: 'layoutVertically',
        horizontal: 'layoutHorizontally',
      });
    },
    VStack(ast, parent) {
      parent = this.Stack(ast, parent);
      parent.layoutVertically();
      return parent;
    },
    HStack(ast, parent) {
      parent = this.Stack(ast, parent);
      parent.layoutHorizontally();
      return parent;
    },
    Spacer(ast, parent) {
      const { length, ...attrs } = ast.attrs;
      parent = parent.addSpacer(ensureBase10Int(length));
      return applyAttrs(parent, attrs);
    },
    Image(ast, parent) {
      const { image, ...attrs } = ast.attrs;
      parent = parent.addImage(image);
      return applyAttrs(parent, attrs, {
        size: 'imageSize',
        opacity: 'imageOpacity',
        fill: 'applyFillingContentMode',
        fitting: 'applyFittingContentMode',
        center: 'centerAlignImage',
        left: 'leftAlignImage',
        right: 'rightAlignImage',
      });
    },
    Symbol(ast, parent) {
      let { name, color, size, ...attrs } = ast.attrs;
      const sfSymbol = SFSymbol.named(name);
      // Look for a style in the props and call the apply*Weight() function 
      ["bold", "black", "heavy", "light", "medium", "regular", "semibold", "thin", "ultralight"].forEach((attr) => {
        if (attrs[attr]) {
          sfSymbol['apply' + capitalize(attr) + 'Weight']();
        }
      });
      if (size) {
        size = ensureBase10Int(size);
        if (typeof size !== "Size")  {
          size = new Size(size, size);
        } 
      }

      return this.Image({
          ...ast,
          attrs: {
            image: sfSymbol.image, 
            size: size || sfSymbol.image.size, 
            tintColor: color, 
            ...attrs
          }
        },
        parent
      )
    },
    Date(ast, parent) {
      const { date, ...attrs } = ast.attrs;
      parent = parent.addDate(date);
      return applyAttrs(parent, attrs, {
        dateStyle: 'applyDateStyle',
        offsetStyle: 'applyOffsetStyle',
        timerStyle: 'applyTimerStyle',
        center: 'centerAlignText',
        left: 'leftAlignText',
        right: 'rightAlignText',
      });
    },
  };

  // Call styles:
  //   render(h``)
  //   render(() => h``)

  let ast = args[0]; // render(h``)
  if (args.length >= 2 && Array.isArray(args[0])) { // render``
    ast = h(...args);
  } else if (args.length === 1) {
    if (typeof args[0] === 'function') { // render(() => h``)
      ast = args[0]();
    }
  }

  if (ast[0].type !== 'ListWidget')
    throw new Error(`A widget must be rendered inside <ListWidget>`);

  return renderWidget(ast[0], null);

  function capitalize(string) {
    return string[0].toUpperCase() + string.slice(1);
  }

  function ensureBase10Int(stringOrNumber) {
    if (typeof stringOrNumber === 'string') {
      return parseInt(stringOrNumber, 10);
    } 
    return stringOrNumber;
  }

  function call(element, name, values) {
    if (Array.isArray(values)) {
      element[name](...values);
    } else {
      element[name](values);
    }
  }
  function applyAttrs(element, attributes, mapping) {
    for (let name in attributes) {
      let values = attributes[name];

      name = (mapping && mapping[name]) || name;
      let eType = typeof element[name];
      if (eType === 'function') { // if it's a function call it 
        call(element, name, values);
      } else if (eType === 'undefined') { // It's undefied

        let setFn = 'set' + capitalize(name); // Lookup setter method

        if (typeof element[setFn] === 'function') { // call it if it excists
          call(element, setFn, values);
        } else { // Fallback to default behaviour
          element[name] = values;
        }
      } else { // Default to just set the property
        element[name] = values;
      }
    }
    return element;
  }

  function renderWidget(ast, parent) {
    if (ast.type) {
      if (NativeElements[ast.type] === undefined)
        throw new Error(`Unkown Element <${ast.type} />`);

      parent = NativeElements[ast.type](ast, parent) || parent;

      if (ast && ast.children) {
        for (let part of ast.children) {
          renderWidget(part, parent);
        }
      }
    } else if (Array.isArray(ast)) {
      for (let part of ast) {
        renderWidget(part, parent);
      }
    }
    return parent;
  }
}

// Using https://github.com/developit/htm
// License https://github.com/developit/htm/blob/master/LICENSE
function h(n){const t=(n,e,s,r)=>{let u;e[0]=0;for(let h=1;h<e.length;h++){const l=e[h++],p=e[h]?(e[0]|=l?1:2,s[e[h++]]):e[++h];3===l?r[0]=p:4===l?r[1]=Object.assign(r[1]||{},p):5===l?(r[1]=r[1]||{})[e[++h]]=p:6===l?r[1][e[++h]]+=p+"":l?(u=n.apply(p,t(n,p,s,["",null])),r.push(u),p[0]?e[0]|=2:(e[h-2]=0,e[h]=u)):r.push(p)}return r};return t(function(n,t,...e){return t=t||{},e=e||[],"function"==typeof n?(e=e.reverse(),n({...t,children:e})):{type:n,attrs:t,children:e}},function(n){let t,e,s=1,r="",u="",h=[0];const l=n=>{1===s&&(n||(r=r.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,r):3===s&&(n||r)?(h.push(3,n,r),s=2):2===s&&"..."===r&&n?h.push(4,n,0):2===s&&r&&!n?h.push(5,0,!0,r):s>=5&&((r||!n&&5===s)&&(h.push(s,0,r,e),s=6),n&&(h.push(s,n,0,e),s=6)),r=""};for(let p=0;p<n.length;p++){p&&(1===s&&l(),l(p));for(let c=0;c<n[p].length;c++)t=n[p][c],1===s?"<"===t?(l(),h=[h],s=3):r+=t:4===s?"--"===r&&">"===t?(s=1,r=""):r=t+r[0]:u?t===u?u="":r+=t:'"'===t||"'"===t?u=t:">"===t?(l(),s=1):s&&("="===t?(s=5,e=r,r=""):"/"===t&&(s<5||">"===n[p][c+1])?(l(),3===s&&(h=h[0]),s=h,(h=h[0]).push(2,0,s),s=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(l(),s=2):r+=t),3===s&&"!--"===r&&(s=4,h=h[0])}return l(),h}(n),arguments,[])}

Vielleicht macht das die Arbeit an diesem Widgets in Zukunft ja einfacher?

@ShadowEnemyx

This comment has been minimized.

Copy link

@ShadowEnemyx ShadowEnemyx commented Oct 24, 2020

C9F40762-4B78-4DBB-931D-6F79BD0037DF

Gestern ging es noch und heute das :(

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

Gestern ging es noch und heute das :(

Sorry, nimm die neueste Version, dann klappt es wieder. Im 1. Kommentar schreibe ich immer die Updates rein.

@ShadowEnemyx

This comment has been minimized.

Copy link

@ShadowEnemyx ShadowEnemyx commented Oct 24, 2020

Gestern ging es noch und heute das :(

Sorry, nimm die neueste Version, dann klappt es wieder. Im 1. Kommentar schreibe ich immer die Updates rein.

Hatte ich eigentlich gemacht dann ging es erst nicht aber 5 min später oder so hat es sich gefangen ^^ vielen Dank

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

@FWeinb
Respekt, hatte ich schon drüben bei twitter gesehen. Da hast du dir echt viel Arbeit gemacht. Ich schau mir das mal in Ruhe an.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 24, 2020

Ein Vorschlag, um die Land-Auswahl für Nutzer*innen einfacher zu machen: mehrere Parameter übergeben, einen davon als country, einen als storeID.

Ja, so habe ich das schon in anderen Widgets umgesetzt. Ich muss es auf jeden Fall so implementieren, dass sämtliche möglichen Fehlerquellen validiert werden. Also mit Komma, ohne Komma, ohne alles, usw. Wenn irgendwas nicht gelesen werden kann, dann zurück auf defaults.

@hirschthomas

This comment has been minimized.

Copy link

@hirschthomas hirschthomas commented Oct 25, 2020

das ja genial :-) gibts das auch für Rossmann ?

@endyman

This comment has been minimized.

Copy link

@endyman endyman commented Oct 25, 2020

Ein kleiner Vorschlag zu dem Fix für die index out of bounds exception die auch für verkaufsoffene Sonntage funktioniert:
if (currentDay <= storeInfo.openingHours.length) {...
wohingegen bei
if (currentDay > 0) {...
die dm-stores im widget Sonntags immer geschlossen bleiben. 🤓

Viel besser, Danke! An verkaufsoffene Sonntage habe ich gar nicht gedacht. Werde ich so einbauen.

An dieser Stelle Sorry für meine überschaubaren Javascript skills. Da bin ich nicht wirklich zuhause. Echte Javascript geeks würden den Code sicher anders schreiben, mit lambdas, promises, fancy pancy etc. Meine Variablennamen sich sicher auch viel zu lang (→ mehr als ein Buchstabe)... ;)

No Problem - allein schon deine Idee war großartig und darüber freuen sich bestimmt viele Menschen - das alleine zählt! Achte nur drauf wenn du die andere if Condition einbauen möchtest, dass da noch ein bisschen was bei dem Index Zugriff gemacht werden muss - sonst fliegt das Sonntags wieder auf die Nase.

@Devs91

This comment has been minimized.

Copy link

@Devs91 Devs91 commented Oct 25, 2020

Hat jemand schon den code für Sneak Preview 2.0?, möchte da keine 8 Widgets anlegen müssen, müsste dann nur noch 4x anlegen.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 25, 2020

Hat jemand schon den code für Sneak Preview 2.0?, möchte da keine 8 Widgets anlegen müssen, müsste dann nur noch 4x anlegen.

Ich habe das zumindest vorerst mal als eigenes gist abgelegt. Kann getestet werden.

https://gist.github.com/marco79cgn/b13719df059d1e8d3277af8216a4d340

@heibie

This comment has been minimized.

Copy link

@heibie heibie commented Oct 25, 2020

voll gute Idee!
Ich hab mir ein Widget mal spasseshalber quick&dirty für Kondome angepasst:
https://twitter.com/heibie/status/1320023406008479745

Idee für die nächsten Versionen:
Die Produkt-IDs und das Icon als Parameter eingebbar machen.
Und dann crowdbasiert oder automatisiert alle Laden-IDs und Procut-Ids aus dem Online-Shop ziehen ;-)

@monza258

This comment has been minimized.

Copy link

@monza258 monza258 commented Oct 25, 2020

voll gute Idee!
Ich hab mir ein Widget mal spasseshalber quick&dirty für Kondome angepasst:

Sehr cool. Würde ich auch nehmen.Teilst du es?

@Aim23

This comment has been minimized.

Copy link

@Aim23 Aim23 commented Oct 26, 2020

das ja genial :-) gibts das auch für Rossmann ?

Rossmann bietet soweit ich weiß keine Bestandsabfrage an, sondern nur "Wird dieser Artikel in dem Store geführt ja/nein".
Dieses Produkt wird im Sortiment Ihrer Filiale geführt. Bitte prüfen Sie die aktuelle Verfügbarkeit vor Ort:

Daher eher weniger sinnig dieses Widget dafür zu benutzen.

@borg-drone

This comment has been minimized.

Copy link

@borg-drone borg-drone commented Oct 26, 2020

Tolles Projekt. Danke dafür.
Geht denn noch Aldi, Lidl und Real? Dort scheint es wohl auch einen Einblick ins Sortiment zu geben.

@MateMatiker

This comment has been minimized.

Copy link

@MateMatiker MateMatiker commented Oct 26, 2020

Geiles Ding, ich liebe es! Bitte mehr davon.

@nonchris

This comment has been minimized.

Copy link

@nonchris nonchris commented Oct 26, 2020

Leider wirft das Skript einen Error :/
Hab ich was falsch copy-pasted?

Error on line 234:40:
ReferenceError: Can't find variable: loadImage
@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 26, 2020

Leider wirft das Skript einen Error :/
Hab ich was falsch copy-pasted?

Error on line 234:40:
ReferenceError: Can't find variable: loadImage

Leider ja, achte darauf, alles bis zum Ende zu markieren. Der nächste Refresh könnte danach ein paar Minuten dauern, bis die Meldung verschwindet.

@nonchris

This comment has been minimized.

Copy link

@nonchris nonchris commented Oct 26, 2020

Jup, jetzt klappts :D
Mega!!!

@Aim23

This comment has been minimized.

Copy link

@Aim23 Aim23 commented Oct 26, 2020

Tolles Projekt. Danke dafür.
Geht denn noch Aldi, Lidl und Real? Dort scheint es wohl auch einen Einblick ins Sortiment zu geben.

Wie bereits oben mehrfach erwähnt, ist dies bei den Märkten nicht möglich.

@vortitu

This comment has been minimized.

Copy link

@vortitu vortitu commented Oct 26, 2020

Ich habe das Widget mal für Android nachgebaut (benötigt Automagic für Android).
Ist hier zu finden https://gist.github.com/vortitu/7e358897c4423d9247c008945a4b22dc falls das jemand gebrauchen kann.

@tomacco81

This comment has been minimized.

Copy link

@tomacco81 tomacco81 commented Oct 26, 2020

voll gute Idee!
Ich hab mir ein Widget mal spasseshalber quick&dirty für Kondome angepasst:

Sehr cool. Würde ich auch nehmen.Teilst du es?

Ich hab spaßeshalber auch mal einen Kondom-Fork erstellt :D
https://gist.github.com/tomacco81/8c240ed4d51d08ff57dbd173e0432432

@FizzyMUC

This comment has been minimized.

Copy link

@FizzyMUC FizzyMUC commented Oct 27, 2020

Sehr nice! Ich würde das gern anpassen, um es auf die Auslastung meines Gyms zu übertragen.
Auf deren Seite sieht man die aktuelle Auslastung in Prozent: https://www.fit-star.de/fitnessstudio/muenchen-neuhausen/
Ich check allerdings nicht, wie ich den Wert (z.B. 47%) abgreifen kann... Die haben keine API... Jemand einen Tipp?

@Aim23

This comment has been minimized.

Copy link

@Aim23 Aim23 commented Oct 27, 2020

Sehr nice! Ich würde das gern anpassen, um es auf die Auslastung meines Gyms zu übertragen.
Auf deren Seite sieht man die aktuelle Auslastung in Prozent: https://www.fit-star.de/fitnessstudio/muenchen-neuhausen/
Ich check allerdings nicht, wie ich den Wert (z.B. 47%) abgreifen kann... Die haben keine API... Jemand einen Tipp?

<div id="c2589" class="frame frame-default frame-type-list frame-layout-0">
    <div class="tx-fs-livedata">
        <div class="bg_light_grey fs-livedata-studio-plugin">
            <div class="studio-inner">
                <h2><span>Live-Studioauslastung</span><img
                        src="/typo3conf/ext/fs_livedata/Resources/Public/Icons/fs_livedata_trendline.svg" width="60"
                        height="40" alt=""></h2>
                <div class="fs-livedata-date">27.10.2020</div>
                <div class="fs-livedata-percentage"><span class="fs-livedata-percentage-label">
                        Aktuelle Auslastung*:
                    </span><strong id="fs-livedata-percentage" class="yellow">45%</strong></div>
                <div class="fs-livedata-chart-msg hidden">
                    <p>Kein Studio ausgewählt.</p>
                </div>
                <div class="fs-livedata-chart">
                    <div class="chartjs-size-monitor">
                        <div class="chartjs-size-monitor-expand">
                            <div class=""></div>
                        </div>
                        <div class="chartjs-size-monitor-shrink">
                            <div class=""></div>
                        </div>
                    </div>
                    <div class="chartjs-size-monitor">
                        <div class="chartjs-size-monitor-expand">
                            <div class=""></div>
                        </div>
                        <div class="chartjs-size-monitor-shrink">
                            <div class=""></div>
                        </div>
                    </div><canvas id="fs-livedata-chart" width="283" height="180" class="chartjs-render-monitor"
                        style="display: block; width: 283px; height: 180px;"></canvas><span
                        id="fs-livedata-chart-y">%</span><span id="fs-livedata-chart-x">|&nbsp;Uhr</span><canvas
                        id="fs-livedata-chart-live" width="50" height="180" class="chartjs-render-monitor"
                        style="display: block; width: 50px; height: 180px;"></canvas>
                </div>
                <script type="text/javascript">
                    var live = JSON.parse(JSON.stringify([[45]]));
                    var studios = JSON.parse(JSON.stringify([{ "studio_id": "5", "studio_name": "FIT STAR München-Neuhausen", "today": ["8.0267558528428", "2.0066889632107", "2.6755852842809", "18.394648829431", "33.444816053512", "50.501672240803", "64.214046822742", "54.515050167224"], "last_week": ["10.702341137124", "2.0066889632107", "0", "34.113712374582", "40.133779264214", "46.488294314381", "34.448160535117", "55.183946488294", "83.612040133779", "81.939799331104", "46.822742474916", "16.387959866221"], "percentage": "54.515050167224", "available": "136" }]));
                </script>
                <div class="fs-livedata-legend"><span id="fs-livedata-lastweek">
                        Auslastung der letzten Woche (in %)
                    </span><br><span id="fs-livedata-current">
                        Aktuelle Auslastung (in %)
                    </span><br><span id="fs-livedata-today">
                        Heutige Auslastung (in %)
                    </span></div>
                <div class="fs-livedata-small"><span class="fs-livedata-small-line-1">*FIT STAR begrenzt freiwillig die
                        maximale Studiokapazität</span><br><span class="fs-livedata-small-line-2">(Berechnung: 1 Person
                        pro 10m<sup>2</sup>)</span><br><span class="fs-livedata-small-line-3">Bei maximaler Auslastung
                        automatischer Zutrittsstop.</span></div>
            </div>
        </div>
    </div>
</div>

So sieht der gesamte div aus für das Seitenelement.

<div class="fs-livedata-percentage"><span class="fs-livedata-percentage-label">
        Aktuelle Auslastung*:
    </span><strong id="fs-livedata-percentage" class="yellow">45%</strong></div>

In dem div verbirgt sich die prozentuale Auslastung.
Am besten die id="fs-livedata-percentage" class="yellow" abgreifen und den Text auslesen. Dann hast du deine Auslastung.

Ich bin eher in Python zu Hause und eher nicht in JS, daher keine spontane Lösung dafür.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 27, 2020

Sehr nice! Ich würde das gern anpassen, um es auf die Auslastung meines Gyms zu übertragen.
Auf deren Seite sieht man die aktuelle Auslastung in Prozent: https://www.fit-star.de/fitnessstudio/muenchen-neuhausen/
Ich check allerdings nicht, wie ich den Wert (z.B. 47%) abgreifen kann... Die haben keine API... Jemand einen Tipp?

So kannst du die Auslastung aus dem HTML parsen:

let auslastung = await loadSite()
console.log("Auslastung: " + auslastung)
  
async function loadSite() {
  let url='https://www.fit-star.de/fitnessstudio/muenchen-neuhausen/'
  let wbv = new WebView()
  await wbv.loadURL(url)
  // javasript to grab data from the website
  let jscript = `
  var auslastung = document
    .getElementById("fs-livedata-percentage")
    .innerText
  
  JSON.stringify(auslastung)
  `
  // Run the javascript
  let result = await wbv.evaluateJavaScript(jscript)
  
  // return the grabbed value
  return result.replaceAll('"', '')
}
@chopseo

This comment has been minimized.

Copy link

@chopseo chopseo commented Oct 27, 2020

Ich habe mir das über die Developer Tools im Browser hergeleitet. Der Shop macht das gleiche, wenn du dort surfst/kaufst. Die IDs der Produkte habe ich über eine Suche bekommen.
https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=177

wie kommt man an die IDs ... hab es nicht herausgefunden?!?

Im Browser die Developer Tools öffnen, dort den Netzwerk Tab und dort dann schauen was aufgerufen wird, wenn du z.B. im Shop auf "Toilettenpapier" klickst. Dieser Call hier liefert dann alle Ergebnisse:
https://products.dm.de/product/de/search?productQuery=%3Arelevance%3AallCategories%3A060201&hideFacets=true&hideSorts=true&pageSize=60

Hello, könntest du noch mal genauer die Schritte beschreiben, wie du diesen Call extrahiert hast? Ich habe in Safari mit den Entwicklertools mir die Webseiteninformationen anzeigen lassen und nichts gefunden, weder im Netzwerk-Tab noch sonst iwo.

@Aim23

This comment has been minimized.

Copy link

@Aim23 Aim23 commented Oct 28, 2020

Eine weitere Pressemitteilung: https://www.business-punk.com/2020/10/der-klopapiercounter-widget-zeigt-vorraetiges-klopapier-in-dm-maerkten-an/ allerdings noch mit dem Klopapier falsch rum.

@muescha

This comment has been minimized.

Copy link

@muescha muescha commented Oct 28, 2020

Kannst du da ein richtiges GitHub Repository draus machen? Dann kann man besser PR mit Codeverbesserungen etc einreichen. Auch releases etc kann man da besser auslesen etc

@Bastilms

This comment has been minimized.

Copy link

@Bastilms Bastilms commented Oct 28, 2020

Leider kann ich mangels Android Smartphone keine Portierung machen. Mir wäre auch keine Android App bekannt, mit der man auf solch einfache Weise content auf den Homescreen bringen kann. Scriptable ist diesbezüglich schlicht genial.

Beim Inzidenz-Widget hatte ein User das bereits für Android portiert. Hier der Link zum Fork. Vielleicht auch hier möglich? Habe leider auch keinen Androiden daheim, sonst würde ich das testen.

Ich habe das ganze nun auf Android Portiert. Das ganze funktioniert mit KWGT. Ich werde die Tag eine Anleitung einstellen wie es konfiguriert wird. Eine Auflistung der Variablen findet ihr hier. Leider ist es nicht ganz so einfach wie bei iOS.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 28, 2020

@haemi

This comment has been minimized.

Copy link

@haemi haemi commented Oct 28, 2020

wie oft läuft das Script denn, also wie oft wird der Inhalt aktualisiert?

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 28, 2020

wie oft läuft das Script denn, also wie oft wird der Inhalt aktualisiert?

Das bestimmt iOS und es hängt von diversen Faktoren ab (Interaktion, Batteriestatus, Energiesparmodus ja/nein, etc.). Grobe Faustregel: alle 5-8 Minuten wird es automatisch aktualisiert.

@haemi

This comment has been minimized.

Copy link

@haemi haemi commented Oct 28, 2020

danke für die Antwort - und noch mehr für das geniale Script!

Mit was für einer Entwicklungsumgebung arbeitest du denn? Würde gerne ein bisschen damit rumspielen 🙂

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 28, 2020

danke für die Antwort - und noch mehr für das geniale Script!

Mit was für einer Entwicklungsumgebung arbeitest du denn? Würde gerne ein bisschen damit rumspielen 🙂

Danke für die Blumen. :)

Das Grobe mach ich mit Visual Studio Code und speichere die Javascript Datei dann direkt im Scriptable Order in iCloud Drive, so dass sie direkt auf's iPhone/iPad gesynct wird.

Den Feinschliff mach ich dann in der Scriptable App. Hier gibt es auch eine Art code completion, die speziell am Anfang sehr hilfreich ist. Eine Doku gibt es auch in der Scriptable App zum nachschlagen.

@AndresAnariba

This comment has been minimized.

Copy link

@AndresAnariba AndresAnariba commented Oct 29, 2020

Ich habe eine Android Version entwickelt! Hier ist mein Repository. Wer da auch reinschauen (und hoffentlich auch beitragen ;) ) will, ist herzlich willkommen!

@Aim23

This comment has been minimized.

Copy link

@Aim23 Aim23 commented Oct 29, 2020

Ich habe eine Android Version entwickelt! Hier ist mein Repository. Wer da auch reinschauen (und hoffentlich auch beitragen ;) ) will, ist herzlich willkommen!

Dein Repository Link müsstest du nochmal bearbeiten und dein , entfernen

@AndresAnariba

This comment has been minimized.

Copy link

@AndresAnariba AndresAnariba commented Oct 29, 2020

Ich habe eine Android Version entwickelt! Hier ist mein Repository. Wer da auch reinschauen (und hoffentlich auch beitragen ;) ) will, ist herzlich willkommen!

Dein Repository Link müsstest du nochmal bearbeiten und dein , entfernen

Upss! Danke!

@phpcode35

This comment has been minimized.

Copy link

@phpcode35 phpcode35 commented Oct 29, 2020

Mal eine frage an die Programmierer hier. Wie bekommt man das hin das er z.b das logo neben den Text macht und nicht untereinander?

Man kann es zwar rechts zentrieren aber dennoch ist es in der neuen Zeile. Habt Ihr ggf. ein kleinen Tipp für mich oder ein kleines beispiel ums vielleicht besser zu verstehen?

`

const preTxt1 = w.addText("Teamspeak Widget".toUpperCase())
preTxt1.textColor = Color.white()
preTxt1.textOpacity = 0.9
preTxt1.font = Font.boldSystemFont(20)
w.addSpacer(0)


const image = w.addImage(img)
image.rightAlignImage()
image.cornerRadius = 8
image.imageSize = new Size(30, 30)`
@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 29, 2020

Das musst du mit Stacks machen.

let stack = widget.addStack()
stack.layoutHorizontally()  // → content auf diesem Stack nebeneinander statt untereinander
const preTxt1 = stack.addText("Teamspeak Widget".toUpperCase())
...  // Text Formatierung wie oben
stack.addSpacer(4)  // Abstand zwischen Text und Bild
const image = stack.addImage(img)
... 

Man kann stacks wiederum beliebig oft weiter stapeln.

@Pr3mut05

This comment has been minimized.

Copy link

@Pr3mut05 Pr3mut05 commented Oct 29, 2020

Vielen Dank für das tolle Widget
Auf meinem Blog gibt es einen kleinen Beitrag dazu

@phpcode35

This comment has been minimized.

Copy link

@phpcode35 phpcode35 commented Oct 29, 2020

Hey Marco,

vielen dank für deinen Tipp. Nun hab ich es verstanden :)

lg

@phpcode35

This comment has been minimized.

Copy link

@phpcode35 phpcode35 commented Oct 29, 2020

WhatsApp Image 2020-10-29 at 19 21 10

Hier siehst du was ich das meine :)

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 29, 2020

Ja klar, du kannst auch zwei Texte nebeneinander platzieren und dann unterschiedliche Schriftarten/-farben definieren.

Sieht gut aus. 👍

@phpcode35

This comment has been minimized.

Copy link

@phpcode35 phpcode35 commented Oct 29, 2020

Ja klar, du kannst auch zwei Texte nebeneinander platzieren und dann unterschiedliche Schriftarten/-farben definieren.

Sieht gut aus. 👍

Hey Marco,

ich danke dir für deinen Tipp. Damit habe ich es so hinbekommen wie ich es mir vorgestellt hatte. Anbei nochmals einen Screenshot damit du die Veränderung zu vorher auch siehst. Ich denke das Widget kann sich Optisch sehen lassen :)

Vielen Dank nochmal 👍

zzzz

@veraverto

This comment has been minimized.

Copy link

@veraverto veraverto commented Oct 30, 2020

Habe es jetzt trotz fehlender Programmierkenntnisse geschafft, dass mir das Widget alle für mich relevanten Mehlsorten anzeigt. Und, durch deine Tipps mit den Stacks, dass das Logo unten rechts neben der Adresse angezeigt wird. Allerdings wird das Logo irgendwie nicht korrekt rechts ausgerichtet und nur durch einen Spacer nach rechts gedrückt. Deshalb wird das Widget bei überlangen Straßennamen breiter als es sein sollte. Ansonsten funktioniert alles super, nochmal vielen Dank für deine Arbeit!
screenshit

Link zum Fork: https://gist.github.com/veraverto/04767193de6f100b6223809154d21483

@Devs91

This comment has been minimized.

Copy link

@Devs91 Devs91 commented Oct 31, 2020

@marco79cgn, kannst du dein Script auch abändert für eine abfrage auf der OBI Seite?

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Oct 31, 2020

@marco79cgn, kannst du dein Script auch abändert für eine abfrage auf der OBI Seite?

Theoretisch wäre das möglich, OBI hat auch Bestandsauskunft. Allerdings gibt's dort kein Klopapier. ;)

@Devs91

This comment has been minimized.

Copy link

@Devs91 Devs91 commented Oct 31, 2020

Hehe, könntest du vielleicht das dm Script auf obi abändern?

@oliverfarr

This comment has been minimized.

Copy link

@oliverfarr oliverfarr commented Oct 31, 2020

Kurze Frage. Bei mir wird das Script im Widget nicht ausgeführt.

47FAF484-53AE-41C0-954E-1B7CCE492E07

674B0621-BD43-4D93-B47C-E4088667B4D3

@monza258

This comment has been minimized.

Copy link

@monza258 monza258 commented Nov 1, 2020

Hat den nicht jemand mal die Zeit und die Lust ein Widget für Unwetterwarnungen vom DWD zu machen? Das wäre mal nützlich und hilfreich.

https://www.dwd.de/DE/wetter/warnungen_landkreise/warnWetter_node.html

Die Daten können per JSON abgerufen werden.

Sollte doch möglich sein.

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Nov 1, 2020

Hat den nicht jemand mal die Zeit und die Lust ein Widget für Unwetterwarnungen vom DWD zu machen? Das wäre mal nützlich und hilfreich.

https://www.dwd.de/DE/wetter/warnungen_landkreise/warnWetter_node.html

Die Daten können per JSON abgerufen werden.

Sollte doch möglich sein.

Die Frage ist (wie grundsätzlich bei allen Widgets), ob man das wirklich 24/7 auf seinem Homescreen haben will und dafür Platz opfert. Gerade eine Unwetter-Warnung ist meiner Ansicht nach eher geeignet für Push-Mitteilungen. Denn die meiste Zeit wird das Widget sonst einfach nichts anzeigen.

@monza258

This comment has been minimized.

Copy link

@monza258 monza258 commented Nov 1, 2020

Hat den nicht jemand mal die Zeit und die Lust ein Widget für Unwetterwarnungen vom DWD zu machen? Das wäre mal nützlich und hilfreich.
https://www.dwd.de/DE/wetter/warnungen_landkreise/warnWetter_node.html
Die Daten können per JSON abgerufen werden.
Sollte doch möglich sein.

Die Frage ist (wie grundsätzlich bei allen Widgets), ob man das wirklich 24/7 auf seinem Homescreen haben will und dafür Platz opfert. Gerade eine Unwetter-Warnung ist meiner Ansicht nach eher geeignet für Push-Mitteilungen. Denn die meiste Zeit wird das Widget sonst einfach nichts anzeigen.

Aber in Widgets lassen sich doch auch Push Mitteilungen einbauen. Zumindest nutze ich eins was es kann. Auch zb nutzbar für Klopapier Knappheit. 😅

@phpcode35

This comment has been minimized.

Copy link

@phpcode35 phpcode35 commented Nov 2, 2020

Aber in Widgets lassen sich doch auch Push Mitteilungen einbauen. Zumindest nutze ich eins was es kann. Auch zb nutzbar für Klopapier Knappheit. 😅

Marco hat diesbezüglich nicht ganz unrecht, und ein Widget nur für ne Push-Benachrichtigung, ich weiß nicht. Da geht der sinn eines Widgets verloren.

Aber es hält dich sicherlich keiner auf es umzusetzen. :P

@Adriankn0rr

This comment has been minimized.

Copy link

@Adriankn0rr Adriankn0rr commented Nov 4, 2020

Sehr schön, aber leider werden alle Artikel die unter "Toilettenpapier" im Suchergebnis bei DM gelistet sind aufgeführt, also auch feuchtes Toilettenpapier, was ja nicht die Mangelware ist sondern das normale Klopapier. Kannst du da noch was optimieren? Ansonsten tolles und witziges Widget

Tolles Skript!

Das Issue ist mir auch aufgefallen, habe nun die Produkt IDs für "Toilettenpapier Roll" gefunden:
610544,863567,853483,799358,524532,28171,137425,525943

hierfür die IDs an dieser Stelle ändern:
url = 'https://products.dm.de/store-availability/DE/availability?dans=595420,708997,137425,28171,485698,799358,863567,452740,610544,846857,709006,452753,879536,452744,485695,853483,594080,504606,593761,525943,842480,535981,127048,524535&storeNumbers=' + storeId

@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Nov 4, 2020

Das Issue, was du zitierst hast, war kein Issue, sondern da wurden lediglich Äpfel (Zielseite, wenn man auf das Widget klickt) mit Birnen (tatsächliche dans im Skript) verglichen.

Was genau sind jetzt "Toilettenpapier Roll"? Ich hatte vorher sämtliche dans händisch rausgesucht, die auch wirklich Toilettenpapier sind (kein Feuchtpapier) und nur diese berücksichtigt für den Counter. Nur beim Aufruf der Shop-Seite klappt das halt nicht.

@Adriankn0rr

This comment has been minimized.

Copy link

@Adriankn0rr Adriankn0rr commented Nov 4, 2020

Das Issue, was du zitierst hast, war kein Issue, sondern da wurden lediglich Äpfel (Zielseite, wenn man auf das Widget klickt) mit Birnen (tatsächliche dans im Skript) verglichen.

Was genau sind jetzt "Toilettenpapier Roll"? Ich hatte vorher sämtliche dans händisch rausgesucht, die auch wirklich Toilettenpapier sind (kein Feuchtpapier) und nur diese berücksichtigt für den Counter. Nur beim Aufruf der Shop-Seite klappt das halt nicht.

Stimmt, es war kein Issue. "Toilettenpapier Roll" hatte die Idee, dass tatsächlich nur gerolltes Toilettenpapier und keine Feuchttücher und anderweitige Produkte berücksichtigt werden, die in der Suche nach "Toilettenpapier" auftauchen. Habe eben aber nachgesehen und es gibt ja tatsächlich Produkte, die berücksichtigt werden sollten, ohne dass sie "Roll..." im Namen haben.

@brineeyx3

This comment has been minimized.

Copy link

@brineeyx3 brineeyx3 commented Nov 6, 2020

Hi, kann mir einer von euch sagen, ob und wie das auch für eine Abfrage bei Ikea möglich wäre?
Ich weiß das sie auf ihrer Website ausweisen ob etwas verfügbar ist und auch mit Stückzahl, nur haben die unterschiedlichen Filialen keine Nummer wie bei DM.
Und ja damit hatte sich dann mein Vorhaben und Halbwissen es selbst zu machen leider auch wieder erledigt. :D

@FWeinb

This comment has been minimized.

Copy link

@FWeinb FWeinb commented Nov 7, 2020

Das auswählen der Store-ID war bisher sehr umständlich, deswegen ich habe mir den Storefinder von DM mal angeschaut und innerhalb einer WebView eingebunden.
Mit ein bisschen JavaScript kann man das ganz nutzen um die Store-ID zu extrahieren, leider bietet Scriptable keine Möglichkeit eine WebView wieder zu schließen:
StoreSelector 2020-11-07 20_03_04

Hier der Quellcode

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: teal; icon-glyph: poo;

let country = 'de' // für Österreich bitte 'at' verwenden
let storeId = undefined;
let param = args.widgetParameter
if (param != null && param.length > 0) {
    storeId = param
}

if (!config.runsInWidget && storeId === undefined) {
    await createStoreChooserUi();
    Script.complete();
    return;
}

const widget = new ListWidget()
const storeInfo = await fetchStoreInformation()
const storeCapacity = await fetchAmountOfPaper()
await createWidget()

// used for debugging if script runs inside the app
if (!config.runsInWidget) {
    await widget.presentSmall()
}
Script.setWidget(widget)
Script.complete()

// build the content of the widget
async function createWidget() {

    widget.addSpacer(4)
    const logoImg = await getImage('dm-logo.png')

    widget.setPadding(10, 10, 10, 10)
    const titleFontSize = 12
    const detailFontSize = 36

    const logoStack = widget.addStack()
    logoStack.addSpacer(86)
    const logoImageStack = logoStack.addStack()
    logoStack.layoutHorizontally()
    logoImageStack.backgroundColor = new Color("#ffffff", 1.0)
    logoImageStack.cornerRadius = 8
    const wimg = logoImageStack.addImage(logoImg)
    wimg.imageSize = new Size(40, 40)
    wimg.rightAlignImage()
    widget.addSpacer()

    const icon = await getImage('toilet-paper.png')
    let row = widget.addStack()
    row.layoutHorizontally()
    row.addSpacer(2)
    const iconImg = row.addImage(icon)
    iconImg.imageSize = new Size(40, 40)
    row.addSpacer(13)

    let column = row.addStack()
    column.layoutVertically()

    const paperText = column.addText("KLOPAPIER")
    paperText.font = Font.mediumRoundedSystemFont(13)

    const packageCount = column.addText(storeCapacity.toString())
    packageCount.font = Font.mediumRoundedSystemFont(22)
    if (storeCapacity < 30) {
        packageCount.textColor = new Color("#E50000")
    } else {
        packageCount.textColor = new Color("#00CD66")
    }
    widget.addSpacer(4)

    const row2 = widget.addStack()
    row2.layoutVertically()

    const street = row2.addText(storeInfo.address.street)
    street.