Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An iOS widget for the Scriptable app which shows the availability of FFP2 masks in your local drug store (and online)
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: yellow; icon-glyph: magic;
// Maske Icon erstellt von "Freepik" (www.flaticon.com/de)
let country = "de";
let storeId = 251;
let param = args.widgetParameter;
if (param != null && param.length > 0) {
if (param.indexOf(";") > 0) {
const paramSplit = param.split(";");
storeId = paramSplit[0];
country = paramSplit[1].toLowerCase();
} else {
storeId = param;
}
}
const widget = new ListWidget();
const storeInfo = await fetchStoreInformation();
const storeCapacity = await fetchAmountOfPaper();
const isOnlineAvailable = await isAvailableOnline();
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");
widget.setPadding(6, 10, 8, 10);
const titleFontSize = 12;
const detailFontSize = 36;
const logoStack = widget.addStack();
logoStack.addSpacer();
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(32, 32);
wimg.rightAlignImage();
const paperText = widget.addText("FFP2 MASKEN");
paperText.font = Font.semiboldRoundedSystemFont(12);
widget.addSpacer(8);
const icon = await getImage("face-mask.png");
let row = widget.addStack();
row.layoutHorizontally();
row.addSpacer(2);
const iconImg = row.addImage(icon);
iconImg.imageSize = new Size(32, 32);
row.addSpacer(12);
let column = row.addStack();
column.layoutVertically();
const storeText = column.addText("STORE");
storeText.textOpacity = 0.8;
storeText.font = Font.mediumRoundedSystemFont(10);
const packageCount = column.addText(storeCapacity.toString());
if (storeCapacity > 999) {
packageCount.font = Font.mediumRoundedSystemFont(15);
} else {
packageCount.font = Font.mediumRoundedSystemFont(19);
}
// packageCount.minimumScaleFactor = 0.8
if (storeCapacity < 30) {
packageCount.textColor = new Color("#E50000");
} else {
packageCount.textColor = new Color("#00CD66");
}
row.addSpacer(12);
let column2 = row.addStack();
column2.layoutVertically();
const onlineText = column2.addText("ONLINE");
onlineText.textOpacity = 0.8;
onlineText.font = Font.mediumRoundedSystemFont(10);
column2.addSpacer(3);
let onlineAvailableIcon;
if (isOnlineAvailable) {
onlineAvailableIcon = "✅";
} else {
onlineAvailableIcon = "⛔️";
}
const onlineAvailableText = column2.addText(onlineAvailableIcon);
if (storeCapacity > 999) {
onlineAvailableText.font = Font.regularSystemFont(11);
} else {
onlineAvailableText.font = Font.regularSystemFont(13);
}
widget.addSpacer(10);
const row2 = widget.addStack();
row2.layoutVertically();
const street = row2.addText(storeInfo.address.street);
street.font = Font.regularSystemFont(11);
street.minimumScaleFactor = 0.8;
street.lineLimit = 2;
const zipCity = row2.addText(
storeInfo.address.zip + " " + storeInfo.address.city
);
zipCity.font = Font.regularSystemFont(11);
zipCity.minimumScaleFactor = 0.8;
zipCity.lineLimit = 1;
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.semiboldSystemFont(10);
}
// fetches the amount of toilet paper packages
async function fetchAmountOfPaper() {
let url;
let counter = 0;
if (country.toLowerCase() === "at") {
// Austria
const array = ["820"];
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=725140,777852,726160,755560,734782,774314,764099,755161,755178,725716,781275,722416&storeNumbers=" +
storeId;
const req = new Request(url);
const apiResult = await req.loadJSON();
for (var i in apiResult.storeAvailabilities) {
if(apiResult.storeAvailabilities[i][0].stockLevel)
{
counter += apiResult.storeAvailabilities[i][0].stockLevel;
}
}
}
return counter;
}
async function isAvailableOnline() {
let url;
if (country.toLowerCase() === "at") {
url =
"https://products.dm.de/product/at/search?productQuery=%3Arelevance%3Adan%3A820";
} else {
url =
"https://products.dm.de/product/de/search?productQuery=%3Arelevance%3Adan%3A726160%3Adan%3A722332%3Adan%3A722416%3Adan%3A755161%3Adan%3A755560%3Adan%3A755178%3Adan%3A725140%3Adan%3A734782%3Adan%3A725716&purchasableOnly=false&hideFacets=false&hideSorts=false&pageSize=30";
}
let req = new Request(url);
let apiResult = await req.loadJSON();
if (req.response.statusCode == 200) {
for (var i = 0; i < apiResult.products.length; i++) {
if (apiResult.products[i].purchasable) {
return true;
}
}
}
return false;
}
// 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/mivolis-ffp2-atemschutzmaske-p4058172788109.html";
} else {
url =
"https://store-data-service.services.dmtech.com/stores/item/de/" +
storeId;
widget.url =
"https://www.dm.de/gesundheit/mundschutz-stoffmasken#ffp2-produkte";
}
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 "face-mask.png":
imageUrl = "https://i.imgur.com/sXG04J0.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();
}
//
// please make sure to copy everything
//
@marco79cgn

This comment has been minimized.

Copy link
Owner Author

@marco79cgn marco79cgn commented Jan 13, 2021

Intro

Das dm FFP2 Masken Widget zeigt die Vorräte an FFP2 Schutzmasken in deiner nächsten lokalen dm Drogerie sowie eine Online-Verfügbarkeit. Nachdem FFP2 Schutzmasken verpflichtend in Bayern sind, werden vermutlich andere Bundesländer nachziehen.

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. Für Österreich bitte hinter die Store ID das Länderkürzel "at" ergänzen, getrennt durch Semikolon, z.B. "251;at"

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. "FFP2-Masken")
  • 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 (FFP2-Masken)
  • Gib als "Parameter" die ermittelten Store ID ein, z.B. "180" für Mainz
  • Optional: Für Geschäfte in Österreich bitte hinter die Store ID das Länderkürzel "at" schreiben, z.B. "251;at"

Danke

Großer Dank an @simonbs für großartige Apps wie Scriptable, DataJar oder Jayson.
Icon erstellt von Freepik (www.flaticon.com)

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 FFP2 Masken.

Updates

13.01.2021, 12:24

  • erste Version (Release)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment