diff --git a/frameos/src/apps/apps.nim b/frameos/src/apps/apps.nim index a0558363..c5a07d26 100644 --- a/frameos/src/apps/apps.nim +++ b/frameos/src/apps/apps.nim @@ -21,6 +21,7 @@ import apps/data/rotateImage/app_loader as data_rotateImage_loader import apps/data/rstpSnapshot/app_loader as data_rstpSnapshot_loader import apps/data/unsplash/app_loader as data_unsplash_loader import apps/data/weather/app_loader as data_weather_loader +import apps/data/wikicommons/app_loader as data_wikicommons_loader import apps/data/xmlToJson/app_loader as data_xmlToJson_loader import apps/logic/breakIfRendering/app_loader as logic_breakIfRendering_loader import apps/logic/ifElse/app_loader as logic_ifElse_loader @@ -59,6 +60,7 @@ proc initApp*(keyword: string, node: DiagramNode, scene: FrameScene): AppRoot = of "data/rstpSnapshot": data_rstpSnapshot_loader.init(node, scene) of "data/unsplash": data_unsplash_loader.init(node, scene) of "data/weather": data_weather_loader.init(node, scene) + of "data/wikicommons": data_wikicommons_loader.init(node, scene) of "data/xmlToJson": data_xmlToJson_loader.init(node, scene) of "logic/breakIfRendering": logic_breakIfRendering_loader.init(node, scene) of "logic/ifElse": logic_ifElse_loader.init(node, scene) @@ -98,6 +100,7 @@ proc setAppField*(keyword: string, app: AppRoot, field: string, value: Value) = of "data/rstpSnapshot": data_rstpSnapshot_loader.setField(app, field, value) of "data/unsplash": data_unsplash_loader.setField(app, field, value) of "data/weather": data_weather_loader.setField(app, field, value) + of "data/wikicommons": data_wikicommons_loader.setField(app, field, value) of "data/xmlToJson": data_xmlToJson_loader.setField(app, field, value) of "logic/breakIfRendering": logic_breakIfRendering_loader.setField(app, field, value) of "logic/ifElse": logic_ifElse_loader.setField(app, field, value) @@ -153,6 +156,7 @@ proc getApp*(keyword: string, app: AppRoot, context: ExecutionContext): Value = of "data/rstpSnapshot": data_rstpSnapshot_loader.get(app, context) of "data/unsplash": data_unsplash_loader.get(app, context) of "data/weather": data_weather_loader.get(app, context) + of "data/wikicommons": data_wikicommons_loader.get(app, context) of "data/xmlToJson": data_xmlToJson_loader.get(app, context) of "render/calendar": render_calendar_loader.get(app, context) of "render/color": render_color_loader.get(app, context) diff --git a/frameos/src/apps/data/wikicommons/app.nim b/frameos/src/apps/data/wikicommons/app.nim new file mode 100644 index 00000000..7edf9d82 --- /dev/null +++ b/frameos/src/apps/data/wikicommons/app.nim @@ -0,0 +1,279 @@ +import pixie +import std/[httpclient, json, options, random, sequtils, strformat, strutils, times, uri] +import frameos/apps +import frameos/types +import frameos/utils/http_client +import frameos/utils/image + +const + CommonsApiUrl = "https://commons.wikimedia.org/w/api.php" + CommonsUserAgent = "FrameOS Wikimedia Commons app (https://github.com/FrameOS/frameos)" + MaxCommonsResponseBytes = 2 * 1024 * 1024 + MaxCommonsImageBytes = 20 * 1024 * 1024 + FirstPotdYear = 2008 + +type + AppConfig* = object + mode*: string + submode*: string + saveAssets*: string + metadataStateKey*: string + + App* = ref object of AppRoot + appConfig*: AppConfig + + CommonsDate = object + year: int + month: int + day: int + + CommonsImage = object + title: string + imageUrl: string + pageUrl: string + description: string + author: string + license: string + mime: string + +proc init*(self: App) = + randomize() + self.appConfig.mode = self.appConfig.mode.strip() + self.appConfig.submode = self.appConfig.submode.strip() + self.appConfig.metadataStateKey = self.appConfig.metadataStateKey.strip() + +proc error*(self: App, context: ExecutionContext, message: string): Image = + self.logError(message) + result = renderError(if context.hasImage: context.image.width else: self.frameConfig.renderWidth(), + if context.hasImage: context.image.height else: self.frameConfig.renderHeight(), message) + +proc commonsHeaders(): HttpHeaders = + newHttpHeaders([ + ("Accept", "application/json"), + ("User-Agent", CommonsUserAgent), + ]) + +proc queryString(params: openArray[(string, string)]): string = + params.mapIt(encodeUrl(it[0]) & "=" & encodeUrl(it[1])).join("&") + +proc fetchCommonsJson(params: openArray[(string, string)]): JsonNode = + var allParams = @[ + ("format", "json"), + ("formatversion", "2") + ] + allParams.add(params) + let body = boundedGetContent( + CommonsApiUrl & "?" & queryString(allParams), + headers = commonsHeaders(), + timeoutMs = 60000, + maxBytes = MaxCommonsResponseBytes, + maxSeconds = 60 + ) + result = parseJson(body) + if result.hasKey("error"): + let message = result["error"]{"info"}.getStr($result["error"]) + raise newException(CatchableError, "Wikimedia Commons API error: " & message) + +proc isLeapYear(year: int): bool = + (year mod 4 == 0 and year mod 100 != 0) or year mod 400 == 0 + +proc daysInMonth(year: int, month: int): int = + case month + of 1, 3, 5, 7, 8, 10, 12: 31 + of 4, 6, 9, 11: 30 + of 2: (if isLeapYear(year): 29 else: 28) + else: 0 + +proc todayDate(): CommonsDate = + let current = now() + CommonsDate(year: current.year, month: current.month.int, day: current.monthday.int) + +proc dateString(date: CommonsDate): string = + &"{date.year}-{date.month:02d}-{date.day:02d}" + +proc randomPreviousDate(today: CommonsDate): CommonsDate = + let year = rand(FirstPotdYear..today.year) + let maxMonth = if year == today.year: today.month else: 12 + let month = rand(1..maxMonth) + var maxDay = daysInMonth(year, month) + if year == today.year and month == today.month: + maxDay = min(maxDay, today.day) + CommonsDate(year: year, month: month, day: rand(1..maxDay)) + +proc randomOnThisDay(today: CommonsDate): CommonsDate = + let maxYear = max(FirstPotdYear, today.year - 1) + for _ in 0 ..< 100: + let year = rand(FirstPotdYear..maxYear) + if today.day <= daysInMonth(year, today.month): + return CommonsDate(year: year, month: today.month, day: today.day) + raise newException(CatchableError, "No previous Wikimedia Commons picture of the day exists for this date.") + +proc stripHtml(value: string): string = + var inTag = false + for ch in value: + case ch + of '<': + inTag = true + of '>': + inTag = false + else: + if not inTag: + result.add(ch) + result = result.replace(""", "\"") + result = result.replace("&", "&") + result = result.replace("'", "'") + result = result.replace("'", "'") + result = result.replace("<", "<") + result = result.replace(">", ">") + result = result.strip() + +proc metadataValue(imageInfo: JsonNode, key: string): string = + let metadata = imageInfo{"extmetadata"} + if metadata.kind == JObject and metadata.hasKey(key): + return metadata[key]{"value"}.getStr().stripHtml() + "" + +proc imageExtension(image: CommonsImage): string = + let urlPath = image.imageUrl.split("?")[0].toLowerAscii() + for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]: + if urlPath.endsWith(ext): + return if ext == ".jpeg": ".jpg" else: ext + case image.mime + of "image/jpeg": ".jpg" + of "image/png": ".png" + of "image/gif": ".gif" + of "image/webp": ".webp" + of "image/svg+xml": ".svg" + else: ".img" + +proc imageFromPage(page: JsonNode): Option[CommonsImage] = + let info = page{"imageinfo"}{0} + if info.kind != JObject: + return none(CommonsImage) + + let mime = info{"mime"}.getStr() + if not mime.startsWith("image/"): + return none(CommonsImage) + + let imageUrl = info{"thumburl"}.getStr(info{"url"}.getStr()) + if imageUrl == "": + return none(CommonsImage) + + let title = page{"title"}.getStr() + let description = metadataValue(info, "ImageDescription") + result = some(CommonsImage( + title: title, + imageUrl: imageUrl, + pageUrl: info{"descriptionurl"}.getStr(), + description: if description != "": description else: metadataValue(info, "ObjectName"), + author: metadataValue(info, "Artist"), + license: metadataValue(info, "LicenseShortName"), + mime: mime + )) + +proc firstImageFromQuery(json: JsonNode): CommonsImage = + let pages = json{"query"}{"pages"} + if pages.kind == JArray: + for page in pages: + let image = imageFromPage(page) + if image.isSome: + return image.get() + raise newException(CatchableError, "No supported image returned from Wikimedia Commons.") + +proc fetchPotdImage(date: CommonsDate, thumbnailWidth: int): CommonsImage = + firstImageFromQuery(fetchCommonsJson([ + ("action", "query"), + ("generator", "images"), + ("titles", "Template:Potd/" & date.dateString()), + ("gimlimit", "20"), + ("prop", "imageinfo"), + ("iiprop", "url|mime|size|extmetadata"), + ("iiurlwidth", $thumbnailWidth) + ])) + +proc fetchRandomImage(thumbnailWidth: int): CommonsImage = + firstImageFromQuery(fetchCommonsJson([ + ("action", "query"), + ("generator", "random"), + ("grnnamespace", "6"), + ("grnlimit", "20"), + ("prop", "imageinfo"), + ("iiprop", "url|mime|size|extmetadata"), + ("iiurlwidth", $thumbnailWidth) + ])) + +proc normalizedMode(self: App): string = + case self.appConfig.mode + of "", "potd", "pictureOfTheDay": + case self.appConfig.submode + of "", "day": "pictureOfTheDay" + of "onthisday", "onThisDay": "onThisDay" + of "month", "random", "randomPotd", "randomPictureOfTheDay": "randomPictureOfTheDay" + else: self.appConfig.mode + of "random": "randomImage" + else: self.appConfig.mode + +proc fetchImageForMode(self: App, thumbnailWidth: int): CommonsImage = + let today = todayDate() + case self.normalizedMode() + of "pictureOfTheDay": + fetchPotdImage(today, thumbnailWidth) + of "onThisDay": + fetchPotdImage(randomOnThisDay(today), thumbnailWidth) + of "randomPictureOfTheDay": + var lastError = "" + for _ in 0 ..< 10: + try: + return fetchPotdImage(randomPreviousDate(today), thumbnailWidth) + except CatchableError as err: + lastError = err.msg + raise newException(CatchableError, "Could not find a random Wikimedia Commons picture of the day: " & lastError) + of "randomImage": + var lastError = "" + for _ in 0 ..< 10: + try: + return fetchRandomImage(thumbnailWidth) + except CatchableError as err: + lastError = err.msg + raise newException(CatchableError, "Could not find a random Wikimedia Commons image: " & lastError) + else: + raise newException(ValueError, "Invalid Wikimedia Commons mode: " & self.appConfig.mode) + +proc get*(self: App, context: ExecutionContext): Image = + let width = if context.hasImage: context.image.width else: self.frameConfig.renderWidth() + let height = if context.hasImage: context.image.height else: self.frameConfig.renderHeight() + + try: + let commonsImage = self.fetchImageForMode(max(max(width, height), 1)) + + if self.frameConfig.debug: + self.log(&"Downloading Wikimedia Commons image: {commonsImage.imageUrl}") + + let imageData = boundedGetContent( + commonsImage.imageUrl, + headers = newHttpHeaders([("User-Agent", CommonsUserAgent)]), + timeoutMs = 60000, + maxBytes = MaxCommonsImageBytes, + maxSeconds = 60 + ) + + if self.appConfig.metadataStateKey != "": + self.scene.state[self.appConfig.metadataStateKey] = %*{ + "source": "wikimedia-commons", + "mode": self.normalizedMode(), + "title": commonsImage.title, + "description": commonsImage.description, + "author": commonsImage.author, + "license": commonsImage.license, + "pageUrl": commonsImage.pageUrl, + "imageUrl": commonsImage.imageUrl, + "mime": commonsImage.mime + } + + if self.appConfig.saveAssets == "auto" or self.appConfig.saveAssets == "always": + discard self.saveAsset(commonsImage.title.replace("File:", ""), commonsImage.imageExtension(), + imageData, self.appConfig.saveAssets == "auto") + + result = decodeImageWithFallback(imageData) + except CatchableError as e: + return self.error(context, "Error fetching image from Wikimedia Commons: " & e.msg) diff --git a/frameos/src/apps/data/wikicommons/config.json b/frameos/src/apps/data/wikicommons/config.json new file mode 100644 index 00000000..4a4372cf --- /dev/null +++ b/frameos/src/apps/data/wikicommons/config.json @@ -0,0 +1,45 @@ +{ + "name": "Wikimedia Commons", + "description": "Images from Wikimedia Commons", + "category": "data", + "version": "1.0.0", + "fields": [ + { + "name": "mode", + "type": "select", + "value": "pictureOfTheDay", + "options": ["pictureOfTheDay", "onThisDay", "randomPictureOfTheDay", "randomImage"], + "required": false, + "label": "Mode", + "hint": "Choose today's Picture of the Day, the Picture of the Day from this date in a previous year, a random previous Picture of the Day, or a random Commons image." + }, + { + "name": "saveAssets", + "type": "select", + "value": "auto", + "options": ["auto", "always", "never"], + "label": "Save asset", + "hint": "Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\n\nYou can later use the 'Local image' app to view saved assets.\n\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved." + }, + { + "name": "metadataStateKey", + "type": "string", + "value": "", + "required": false, + "label": "Metadata state key", + "placeholder": "e.g. wikimediaMetadata" + } + ], + "output": [ + { + "name": "image", + "type": "image" + } + ], + "cache": { + "enabled": true, + "inputEnabled": true, + "durationEnabled": true, + "duration": "3600" + } +} diff --git a/frameos/src/apps/data/wikicommons/tests/test_app.nim b/frameos/src/apps/data/wikicommons/tests/test_app.nim new file mode 100644 index 00000000..233464c7 --- /dev/null +++ b/frameos/src/apps/data/wikicommons/tests/test_app.nim @@ -0,0 +1,47 @@ +import std/[json, strutils, unittest] +import pixie + +import ../app +import frameos/types + +type LogStore = ref object + items: seq[JsonNode] + +proc newLogger(store: LogStore): Logger = + Logger( + log: proc(payload: JsonNode) = + store.items.add(payload) + ) + +suite "data/wikicommons app": + test "init trims text fields": + let app = App(appConfig: AppConfig( + mode: " pictureOfTheDay ", + submode: " day ", + metadataStateKey: " commons " + )) + + app.init() + + check app.appConfig.mode == "pictureOfTheDay" + check app.appConfig.submode == "day" + check app.appConfig.metadataStateKey == "commons" + + test "invalid mode returns error image with context dimensions": + let logs = LogStore(items: @[]) + let scene = FrameScene(state: %*{}, logger: newLogger(logs)) + let app = App( + nodeId: 12.NodeId, + nodeName: "data/wikicommons", + scene: scene, + frameConfig: FrameConfig(width: 10, height: 6), + appConfig: AppConfig(mode: "not-a-mode", metadataStateKey: "meta") + ) + + let image = app.get(ExecutionContext(image: newImage(15, 9), hasImage: true)) + + check image.width == 15 + check image.height == 9 + check not scene.state.hasKey("meta") + check logs.items.len == 1 + check logs.items[0]["event"].getStr().contains("error:12:data/wikicommons") diff --git a/frameos/src/assets/apps.nim b/frameos/src/assets/apps.nim index cb149317..3cba4800 100644 --- a/frameos/src/assets/apps.nim +++ b/frameos/src/assets/apps.nim @@ -1,6 +1,6 @@ # Auto-generated by tools/generate_apps_asset_nim.py -const appsJson* = """{"apps":{"data/beRecycle":{"cache":{"duration":"14400","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Return a JSON event list of trash pickup dates","fields":[{"label":"Street name","name":"streetName","placeholder":"","required":true,"type":"string","value":""},{"label":"Number","name":"number","placeholder":"","required":true,"type":"integer","value":""},{"label":"Postal code","name":"postalCode","placeholder":"","required":true,"type":"integer","value":""},{"label":"Export events from (YYYY-MM-DD)","name":"exportFrom","placeholder":"now","required":false,"type":"string","value":""},{"label":"Export events until (YYYY-MM-DD)","name":"exportUntil","placeholder":"1 year later","required":false,"type":"string","value":""},{"label":"Maximum number of events to export","name":"exportCount","placeholder":"50","required":false,"type":"integer","value":"50"},{"label":"Language","name":"language","options":["en","fr","nl"],"placeholder":"","required":true,"type":"select","value":"en"},{"hint":"In case the default value stops working, open recycleapp.be, and copy the value of the x-secret header sent to `/v1/access-token`.","label":"x-secret value","name":"xSecret","placeholder":"","required":false,"type":"string","value":""},{"markdown":"[{ summary, startTime, endTime, timezone }]"}],"name":"Recycling Calendar for Belgium","output":[{"name":"events","type":"json"}],"version":"1.0.0"},"data/chromiumScreenshot":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Capture a snapshot of a website with playwright and headless chromium. Needs a 64-bit system and at least 1GB of RAM.","fields":[{"label":"URL to capture","name":"url","placeholder":"","required":true,"type":"text","value":"https://frameos.net/"},{"label":"Width (0=full width)","name":"width","placeholder":"0","type":"integer","value":""},{"label":"Height (0=full height)","name":"height","placeholder":"0","type":"integer","value":""},{"hint":"When enabled, cookies/session storage persist between captures. Disable to use a fresh incognito-like session for each render.","label":"Persist browser session between renders","name":"persistSession","type":"boolean","value":true},{"hint":"Always install and run Chromium, even if we have less than 1GB of RAM. Disable this at your own risk!","label":"Disable low memory check","name":"disableLowMemoryCheck","type":"boolean","value":false}],"name":"Chromium screenshot","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/clock":{"category":"data","description":"Return the current time","fields":[{"markdown":"[Date format syntax](https://nim-lang.org/2.0.2/times.html)"},{"label":"Format","name":"format","options":["yyyy-MM-dd","yyyy-MM-dd HH:mm:ss","HH:mm:ss:fff","HH:mm:ss","HH:mm","custom"],"placeholder":"HH:mm:ss","required":true,"type":"select","value":"HH:mm:ss"},{"label":"Custom format","name":"formatCustom","placeholder":"","required":false,"showIf":[{"field":"format","operator":"eq","value":"custom"}],"type":"string","value":""}],"name":"Clock","output":[{"name":"time","type":"string"}],"version":"1.0.0"},"data/downloadImage":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Download image from an URL","fields":[{"label":"Image URL","name":"url","placeholder":"https://domain/image","required":true,"type":"text","value":""},{"hint":"Enter a state key to persist metadata about the image. Stores: {url, width, height, exif: {...} }","label":"Optional metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""}],"name":"Download Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/downloadUrl":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Download a URL into a string","fields":[{"label":"URL","name":"url","placeholder":"https://domain/content","required":true,"type":"text","value":""}],"name":"Download URL","output":[{"name":"result","type":"string"}],"version":"1.0.0"},"data/eventsToAgenda":{"category":"data","description":"Convert events to an basic-caret formatted agenda string","fields":[{"label":"Events","name":"events","required":true,"type":"json"},{"label":"Base font size","name":"baseFontSize","required":true,"type":"float","value":"24"},{"label":"Title font size","name":"titleFontSize","required":true,"type":"float","value":"48"},{"label":"Day title color","name":"titleColor","required":true,"type":"color","value":"#FFFFFF"},{"label":"Base text color","name":"textColor","required":true,"type":"color","value":"#FFFFFF"},{"label":"Time color","name":"timeColor","required":true,"type":"color","value":"#FF0000"},{"label":"Always start with today's date","name":"startWithToday","required":true,"type":"boolean","value":"true"}],"name":"Events to Agenda","output":[{"name":"result","type":"string"}],"version":"1.0.0"},"data/frameOSGallery":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Random image from the FrameOS gallery","fields":[{"markdown":"[Click here](https://gallery.frameos.net/) to see all the galleries."},{"label":"Category","name":"category","options":["building-art-styles","cute","cyberpunk-europe","masterpieces","space-gallery","space-odyssey","other"],"required":false,"type":"select","value":"cute"},{"label":"Category (if other)","name":"categoryOther","placeholder":"","required":false,"showIf":[{"field":"category","operator":"eq","value":"other"}],"type":"string","value":""}],"name":"FrameOS Gallery","output":[{"name":"image","type":"image"}],"settings":["frameOS"],"version":"1.0.0"},"data/haSensor":{"cache":{"duration":"60","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Get the state of a Home Assistant entity","fields":[{"markdown":"Find the [entity id here](http://homeassistant.local:8123/config/entities)"},{"label":"Entity ID","name":"entityId","placeholder":"Home Assistant entity name. Example: sensor.home_solar_percentage or water_heater.hot_water","required":true,"type":"text"},{"label":"Debug logging","name":"debug","required":false,"type":"boolean","value":"false"}],"name":"Home Assistant Sensor","output":[{"name":"state","type":"json"}],"settings":["homeAssistant"],"version":"1.0.0"},"data/icalJson":{"cache":{"enabled":true,"inputEnabled":true},"category":"data","description":"Convert an iCal file into a JSON event list","fields":[{"label":"iCal file contents","name":"ical","required":true,"type":"string"},{"label":"Export events from (YYYY-MM-DD)","name":"exportFrom","placeholder":"now","required":false,"type":"string","value":""},{"label":"Export events until (YYYY-MM-DD)","name":"exportUntil","placeholder":"1 year later","required":false,"type":"string","value":""},{"label":"Maximum number of events to export","name":"exportCount","placeholder":"50","required":false,"type":"integer","value":"50"},{"label":"Filter events by keyword","name":"search","placeholder":"","required":false,"type":"string","value":""},{"label":"Add 'location' to the result JSON","name":"addLocation","required":false,"type":"boolean","value":"true"},{"label":"Add 'url' to the result JSON","name":"addUrl","required":false,"type":"boolean","value":"true"},{"label":"Add 'description' to the result JSON","name":"addDescription","required":false,"type":"boolean","value":"false"},{"label":"Add 'timezone' to the result JSON","name":"addTimezone","required":false,"type":"boolean","value":"false"},{"markdown":"[{ summary, startTime, endTime, location, url, description, timezone }]"}],"name":"iCal to Events JSON","output":[{"name":"events","type":"json"}],"version":"1.0.0"},"data/localImage":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Show an image from the SD card","fields":[{"label":"Image filename or folder","name":"path","placeholder":"Defaults to the assets folder (e.g. /srv/assets)","required":true,"type":"text","value":""},{"label":"Order of images","name":"order","options":["random","alphabetical"],"required":true,"type":"select","value":"random"},{"hint":"Enter a state key to persist the current image index between restarts.","label":"Optional state key for persistence","name":"counterStateKey","required":false,"type":"string","value":""},{"hint":"Enter a state key to persist metadata about the last image. Stores: {path, filename, index, total, width, height, exif: {...} }","label":"Optional metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""},{"label":"Filter filenames by keyword","name":"search","required":false,"type":"string","value":""}],"name":"Local Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/log":{"category":"data","description":"Log a JSON object","fields":[{"label":"Input JSON","name":"inputJson","required":false,"type":"json"}],"name":"Log JSON","output":[{"name":"outputJson","type":"json"}],"version":"1.0.0"},"data/newImage":{"cache":{"enabled":true,"inputEnabled":true},"category":"data","description":"Create a new image with a single color background","fields":[{"label":"Width","name":"width","placeholder":"auto","required":false,"type":"integer"},{"label":"Height","name":"height","placeholder":"auto","required":false,"type":"integer"},{"label":"Color","name":"color","required":true,"type":"color","value":"#ffffff"},{"label":"Opacity (0-1)","name":"opacity","required":false,"type":"float","value":"1"},{"label":"Render next","name":"renderNext","required":false,"type":"node"}],"name":"New Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/openaiImage":{"cache":{"duration":"3600","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Random AI generated art from OpenAI's image models","fields":[{"label":"Prompt","name":"prompt","placeholder":"e.g. pumpkin pyjama party, digital art","required":true,"rows":4,"type":"text","value":""},{"label":"Model","name":"model","options":["gpt-image-1.5","gpt-image-1","dall-e-3","dall-e-2"],"required":true,"type":"select","value":"gpt-image-1.5"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","1536x1024","1024x1536"],"showIf":[{"field":"model","operator":"eq","value":"gpt-image-1"},{"field":"model","operator":"eq","value":"gpt-image-1.5"}],"type":"select","value":"best for orientation"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","1792x1024","1024x1792"],"showIf":[{"field":"model","operator":"eq","value":"dall-e-3"}],"type":"select","value":"best for orientation"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","512x512","256x256"],"showIf":[{"field":"model","operator":"eq","value":"dall-e-2"}],"type":"select","value":"best for orientation"},{"label":"Style","name":"style","options":["vivid","natural",""],"showIf":[{"field":"model","operator":"eq","value":"dall-e-3"}],"type":"select","value":"vivid"},{"label":"Quality","name":"quality","options":["standard","hd",""],"showIf":[{"field":"model","operator":"eq","value":"dall-e-3"}],"type":"select","value":"standard"},{"hint":"Enter a state key to persist metadata about the image. Stores: {prompt, model, size}.","label":"Metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""},{"hint":"Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\n\nYou can later use the 'Local image' app to view saved assets.\n\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved.","label":"Save asset","name":"saveAssets","options":["auto","always","never"],"type":"select","value":"auto"}],"name":"OpenAI Image","output":[{"name":"image","type":"image"}],"settings":["openAI"],"version":"1.0.0"},"data/openaiText":{"cache":{"duration":"600","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Text response from ChatGPT and friends","fields":[{"label":"System Prompt","name":"system","placeholder":"","required":true,"rows":6,"type":"text","value":"You're a smart e-ink frame running FrameOS. Reply with plain text only. Space is very limited."},{"label":"User Prompt","name":"user","placeholder":"Write your prompt here. Keep it short and clear.","required":true,"rows":6,"type":"text","value":""},{"label":"Model","name":"model","required":true,"type":"string","value":"gpt-5"}],"name":"OpenAI Text","output":[{"name":"reply","type":"string"}],"settings":["openAI"],"version":"1.0.0"},"data/parseJson":{"category":"data","description":"Parse a text string and convert to JSON","fields":[{"label":"Text","name":"text","required":true,"type":"text"}],"name":"Parse JSON","output":[{"name":"json","type":"json"}],"version":"1.0.0"},"data/prettyJson":{"category":"data","description":"Pretty print JSON into a string","fields":[{"label":"JSON","name":"json","required":true,"type":"json"},{"label":"Indent","name":"ident","required":false,"type":"integer","value":"2"},{"label":"Prettify","name":"prettify","required":false,"type":"boolean","value":"true"}],"name":"Pretty JSON","output":[{"name":"result","type":"string"}],"version":"1.0.0"},"data/qr":{"cache":{"enabled":true,"inputEnabled":true},"category":"data","description":"QR codes. Default to a link to the frame control URL.","fields":[{"label":"Code Type","name":"codeType","options":["Frame Control URL","Frame Image URL","Custom"],"required":false,"type":"select","value":"Frame Control URL"},{"label":"Custom code","name":"code","required":false,"showIf":[{"field":"codeType","operator":"eq","value":"Custom"}],"type":"string","value":""},{"label":"Size","name":"size","type":"float","value":"2"},{"label":"Size unit","name":"sizeUnit","options":["pixels per dot","pixels total","percent"],"required":true,"type":"select","value":"pixels per dot"},{"label":"Alignment pattern radius %","name":"alRad","type":"float","value":"30"},{"label":"Module radius %","name":"moRad","type":"float","value":"0"},{"label":"Module separation %","name":"moSep","type":"float","value":"0"},{"label":"Padding in dots","name":"padding","placeholder":"1","required":true,"type":"integer","value":"1"},{"label":"QR Code Color","name":"qrCodeColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Background Color","name":"backgroundColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"}],"name":"QR Code","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/resizeImage":{"category":"data","description":"Scale or stretch an image","fields":[{"label":"Image","name":"image","required":true,"type":"image"},{"label":"New Width","name":"width","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"New Height","name":"height","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"contain"}],"name":"Resize","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/rotateImage":{"category":"data","description":"Rotate an image","fields":[{"label":"Image","name":"image","required":true,"type":"image"},{"label":"Rotation Degree","name":"rotationDegree","placeholder":"e.g., 45","required":true,"type":"float","value":"0"}],"name":"Rotate","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/rstpSnapshot":{"apt":["ffmpeg"],"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Take a still image from a webcam feed with ffmpeg","fields":[{"label":"RTSP URL","name":"url","placeholder":"rstp://domain/cam","required":true,"type":"text","value":""}],"name":"RSTP Snapshot","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/unsplash":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Random unsplash image","fields":[{"label":"Search","name":"search","placeholder":"e.g. pineapple, nature, birds, power","required":false,"type":"string","value":"nature"},{"label":"Orientation","name":"orientation","options":["auto","any","landscape","portrait","squarish"],"placeholder":"landscape, portrait, square","required":false,"type":"select","value":"auto"},{"hint":"Enter a state key to persist metadata about the image. Stores: {search, description, author, ...}.","label":"Metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""},{"hint":"Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\n\nYou can later use the 'Local image' app to view saved assets.\n\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved.","label":"Save asset","name":"saveAssets","options":["auto","always","never"],"type":"select","value":"auto"}],"name":"Unsplash","output":[{"name":"image","type":"image"}],"settings":["unsplash"],"version":"1.0.0"},"data/weather":{"category":"data","description":"Fetch current, hourly, and daily forecasts from Open-Meteo for a location and date.","fields":[{"label":"Location","name":"location","placeholder":"City, state, or country","required":true,"type":"string","value":""},{"label":"Date (YYYY-MM-DD)","name":"date","placeholder":"2024-01-30","required":false,"type":"string","value":""},{"label":"Timezone","name":"timezone","placeholder":"auto","required":false,"type":"string","value":"auto"},{"label":"Temperature unit","name":"temperatureUnit","options":["celsius","fahrenheit"],"required":true,"type":"select","value":"celsius"},{"label":"Wind speed unit","name":"windSpeedUnit","options":["kmh","ms","mph","kn"],"required":true,"type":"select","value":"kmh"},{"label":"Precipitation unit","name":"precipitationUnit","options":["mm","inch"],"required":true,"type":"select","value":"mm"}],"name":"Weather (Open-Meteo)","output":[{"example":"{\n \"provider\": \"open-meteo\",\n \"forecastModes\": [\n \"current\",\n \"hourly\",\n \"daily\"\n ],\n \"date\": \"2026-01-18\",\n \"location\": {\n \"name\": \"Brussels\",\n \"latitude\": 50.85045,\n \"longitude\": 4.34878,\n \"timezone\": \"auto\",\n \"country\": \"Belgium\",\n \"countryCode\": \"BE\",\n \"admin1\": \"Brussels Capital\",\n \"admin2\": \"Bruxelles-Capitale\"\n },\n \"forecast\": {\n \"latitude\": 50.854,\n \"longitude\": 4.35,\n \"generationtime_ms\": 0.3679990768432617,\n \"utc_offset_seconds\": 3600,\n \"timezone\": \"Europe/Brussels\",\n \"timezone_abbreviation\": \"GMT+1\",\n \"elevation\": 27,\n \"current_weather_units\": {\n \"time\": \"iso8601\",\n \"interval\": \"seconds\",\n \"temperature\": \"\\u00b0C\",\n \"windspeed\": \"km/h\",\n \"winddirection\": \"\\u00b0\",\n \"is_day\": \"\",\n \"weathercode\": \"wmo code\"\n },\n \"current_weather\": {\n \"time\": \"2026-01-18T22:15\",\n \"interval\": 900,\n \"temperature\": 5.7,\n \"windspeed\": 5.4,\n \"winddirection\": 132,\n \"is_day\": 0,\n \"weathercode\": 0\n },\n \"hourly_units\": {\n \"time\": \"iso8601\",\n \"temperature_2m\": \"\\u00b0C\",\n \"apparent_temperature\": \"\\u00b0C\",\n \"precipitation\": \"mm\",\n \"weathercode\": \"wmo code\",\n \"windspeed_10m\": \"km/h\",\n \"winddirection_10m\": \"\\u00b0\"\n },\n \"hourly\": {\n \"time\": [\n \"2026-01-18T00:00\",\n \"2026-01-18T01:00\",\n \"2026-01-18T02:00\",\n \"2026-01-18T03:00\",\n \"2026-01-18T04:00\",\n \"2026-01-18T05:00\",\n \"2026-01-18T06:00\",\n \"2026-01-18T07:00\",\n \"2026-01-18T08:00\",\n \"2026-01-18T09:00\",\n \"2026-01-18T10:00\",\n \"2026-01-18T11:00\",\n \"2026-01-18T12:00\",\n \"2026-01-18T13:00\",\n \"2026-01-18T14:00\",\n \"2026-01-18T15:00\",\n \"2026-01-18T16:00\",\n \"2026-01-18T17:00\",\n \"2026-01-18T18:00\",\n \"2026-01-18T19:00\",\n \"2026-01-18T20:00\",\n \"2026-01-18T21:00\",\n \"2026-01-18T22:00\",\n \"2026-01-18T23:00\"\n ],\n \"temperature_2m\": [\n 9.2,\n 8.2,\n 8.3,\n 8.1,\n 7.2,\n 7.1,\n 7.3,\n 7.1,\n 6.7,\n 7.3,\n 8.5,\n 8.5,\n 9.7,\n 10.9,\n 11.1,\n 11.3,\n 11.1,\n 9.7,\n 8.7,\n 8,\n 6.8,\n 6.2,\n 5.8,\n 5.5\n ],\n \"apparent_temperature\": [\n 7.5,\n 6.3,\n 6.5,\n 6.2,\n 5.1,\n 4.8,\n 4.9,\n 4.8,\n 3.7,\n 4.4,\n 5.6,\n 5.6,\n 6.9,\n 8.7,\n 8,\n 8.6,\n 8.3,\n 6.7,\n 5.9,\n 5,\n 3.6,\n 3.5,\n 3.3,\n 2.4\n ],\n \"precipitation\": [\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0\n ],\n \"weathercode\": [\n 0,\n 1,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 0,\n 0,\n 1,\n 2,\n 2,\n 0,\n 3,\n 3,\n 2,\n 0,\n 0,\n 0,\n 0\n ],\n \"windspeed_10m\": [\n 4,\n 3.6,\n 2.9,\n 3.6,\n 5.8,\n 5,\n 6.5,\n 5,\n 9.4,\n 9,\n 8.6,\n 9,\n 9.4,\n 5.8,\n 9.7,\n 7.9,\n 8.6,\n 9.4,\n 10.4,\n 8.6,\n 9.7,\n 5.8,\n 5,\n 7.2\n ],\n \"winddirection_10m\": [\n 166,\n 59,\n 353,\n 67,\n 78,\n 52,\n 71,\n 68,\n 101,\n 54,\n 87,\n 80,\n 62,\n 66,\n 69,\n 74,\n 92,\n 73,\n 77,\n 110,\n 117,\n 202,\n 132,\n 168\n ]\n },\n \"daily_units\": {\n \"time\": \"iso8601\",\n \"temperature_2m_max\": \"\\u00b0C\",\n \"temperature_2m_min\": \"\\u00b0C\",\n \"precipitation_sum\": \"mm\",\n \"weathercode\": \"wmo code\",\n \"sunrise\": \"iso8601\",\n \"sunset\": \"iso8601\",\n \"windspeed_10m_max\": \"km/h\"\n },\n \"daily\": {\n \"time\": [\n \"2026-01-18\"\n ],\n \"temperature_2m_max\": [\n 11.3\n ],\n \"temperature_2m_min\": [\n 5.5\n ],\n \"precipitation_sum\": [\n 0\n ],\n \"weathercode\": [\n 3\n ],\n \"sunrise\": [\n \"2026-01-18T08:35\"\n ],\n \"sunset\": [\n \"2026-01-18T17:10\"\n ],\n \"windspeed_10m_max\": [\n 10.4\n ]\n }\n }\n}","name":"weather","type":"json"}],"version":"1.0.0"},"data/xmlToJson":{"category":"data","description":"Convert XML into a JSON tree using Nim's xmlparser","fields":[{"label":"XML","name":"xml","required":true,"type":"string"}],"name":"XML to JSON","output":[{"name":"json","type":"json"}],"version":"1.0.0"},"legacy/clock":{"category":"legacy","description":"Overlay current time on the image","fields":[{"markdown":"[Date format syntax](https://nim-lang.org/2.0.2/times.html)"},{"label":"Format","name":"format","options":["yyyy-MM-dd","yyyy-MM-dd HH:mm:ss","HH:mm:ss:fff","HH:mm:ss","HH:mm","custom"],"placeholder":"HH:mm:ss","required":true,"type":"select","value":"HH:mm:ss"},{"label":"Custom format","name":"formatCustom","placeholder":"","required":false,"type":"string","value":""},{"label":"Position","name":"position","options":["top-left","top-center","top-right","center-left","center-center","center-right","bottom-left","bottom-center","bottom-right"],"placeholder":"center-center","required":true,"type":"select","value":"center-center"},{"label":"Offset X","name":"offsetX","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Offset Y","name":"offsetY","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Padding","name":"padding","placeholder":"10","required":true,"type":"float","value":"10"},{"label":"Font Color","name":"fontColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"},{"label":"Font Size","name":"fontSize","placeholder":"32","required":true,"type":"float","value":"32"},{"label":"Border Color","name":"borderColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Border width","name":"borderWidth","placeholder":"2","required":true,"type":"integer","value":"2"}],"name":"Clock (legacy)","version":"1.0.0"},"legacy/downloadImage":{"category":"legacy","description":"Download image from an URL","fields":[{"label":"Image URL","name":"url","placeholder":"https://domain/image","required":true,"type":"text","value":""},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"Download Image (legacy)","version":"1.0.0"},"legacy/frameOSGallery":{"category":"legacy","description":"Random image from the FrameOS gallery","fields":[{"markdown":"[Click here](https://gallery.frameos.net/) to see all the galleries."},{"label":"Category","name":"category","options":["building-art-styles","cute","cyberpunk-europe","masterpieces","space-gallery","space-odyssey"],"required":false,"type":"select","value":"cute"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"FrameOS Gallery (legacy)","settings":["frameOS"],"version":"1.0.0"},"legacy/haSensor":{"category":"legacy","description":"Store the state of a Home Assistant entity in the scene's state","fields":[{"markdown":"Find the [entity id here](http://homeassistant.local:8123/config/entities). Then use code like:\n\nscene.state{\"water_heater\"}{\"state\"}.getStr"},{"label":"Entity ID","name":"entityId","placeholder":"Home Assistant entity name. Example: sensor.home_solar_percentage or water_heater.hot_water","required":true,"type":"text"},{"label":"State key to store the json in","name":"stateKey","placeholder":"","required":true,"type":"text","value":"sensor"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 60. Use 0 for no cache","required":false,"type":"float","value":"60"},{"label":"Debug logging","name":"debug","required":false,"type":"boolean","value":"false"}],"name":"HA Sensor (legacy)","settings":["homeAssistant"],"version":"1.0.0"},"legacy/localImage":{"category":"legacy","description":"Show an image from the SD card","fields":[{"label":"Image filename or folder","name":"path","placeholder":"/srv/images","required":true,"type":"text","value":"/srv/images"},{"label":"Order of images","name":"order","options":["random","alphabetical"],"required":true,"type":"select","value":"random"},{"label":"Seconds to show one image","name":"seconds","placeholder":"Default: 900 (15min)","required":false,"type":"float","value":"900"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Optional state key for persistence","name":"counterStateKey","required":false,"type":"string","value":""}],"name":"Local Image (legacy)","version":"1.0.0"},"legacy/openai":{"category":"legacy","description":"Random AI generated art from OpenAI's DALL-E models","fields":[{"label":"Prompt","name":"prompt","placeholder":"e.g. pumpkin pyjama party, digital art","required":true,"rows":6,"type":"text","value":""},{"label":"Model","name":"model","options":["dall-e-3","dall-e-2"],"required":true,"type":"select","value":"dall-e-3"},{"label":"Size (dall-e-3)","name":"size","options":["best for orientation","1024x1024","1792x1024","1024x1792"],"type":"select","value":"best for orientation"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Style","name":"style","options":["vivid","natural",""],"type":"select","value":"vivid"},{"label":"Quality","name":"quality","options":["standard","hd",""],"type":"select","value":"standard"},{"label":"Seconds to cache each prompt","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"OpenAI Image (legacy)","settings":["openAI"],"version":"1.0.0"},"legacy/openaiText":{"category":"legacy","description":"Text response from ChatGPT and friends","fields":[{"label":"System Prompt","name":"system","placeholder":"","required":true,"rows":6,"type":"text","value":"You're a smart e-ink frame running FrameOS. Reply with plain text only. Space is very limited."},{"label":"User Prompt","name":"user","placeholder":"Write your prompt here. Keep it short and clear.","required":true,"rows":6,"type":"text","value":""},{"label":"Model","name":"model","required":true,"type":"string","value":"gpt-4"},{"label":"State key for reply","name":"stateKey","required":true,"type":"string","value":"reply"},{"label":"Seconds to cache each prompt","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"OpenAI Text (legacy)","settings":["openAI"],"version":"1.0.0"},"legacy/qr":{"category":"legacy","description":"Display QR codes. Default to link to self.","fields":[{"label":"Code Type","name":"codeType","options":["Frame Control URL","Frame Image URL","Custom"],"required":false,"type":"select","value":"Frame Control URL"},{"label":"Code (if Custom above)","name":"code","required":false,"type":"string","value":""},{"label":"Size","name":"size","type":"float","value":"2"},{"label":"Size unit","name":"sizeUnit","options":["percent","pixels per dot","pixels total"],"required":true,"type":"select","value":"pixels per dot"},{"label":"Alignment pattern radius %","name":"alRad","type":"float","value":"30"},{"label":"Module radius %","name":"moRad","type":"float","value":"0"},{"label":"Module separation %","name":"moSep","type":"float","value":"0"},{"label":"Position","name":"position","options":["top-left","top-center","top-right","center-left","center-center","center-right","bottom-left","bottom-center","bottom-right"],"placeholder":"center-center","required":true,"type":"select","value":"center-center"},{"label":"Offset X","name":"offsetX","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Offset Y","name":"offsetY","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Padding in dots","name":"padding","placeholder":"1","required":true,"type":"integer","value":"1"},{"label":"QR Code Color","name":"qrCodeColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Background Color","name":"backgroundColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"}],"name":"QR Code (legacy)","version":"1.0.0"},"legacy/resize":{"category":"legacy","description":"Scale or stretch the image","fields":[{"label":"New Width","name":"width","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"New Height","name":"height","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"contain"}],"name":"Resize (legacy)","version":"1.0.0"},"legacy/rotate":{"category":"legacy","description":"Rotate the image","fields":[{"label":"Rotation Degree","name":"rotationDegree","placeholder":"e.g., 45","required":true,"type":"float","value":"0"},{"label":"Scaling mode","name":"scalingMode","options":["expand","cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"}],"name":"Rotate (legacy)","version":"1.0.0"},"legacy/unsplash":{"category":"legacy","description":"Random unsplash image","fields":[{"label":"Random keyword (one word)","name":"keyword","placeholder":"e.g. pineapple, nature, birds, power","required":false,"type":"string","value":"nature"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 to refetch on every render.","required":false,"type":"float","value":"3600"}],"name":"Unsplash (legacy/broken)","version":"1.0.0"},"logic/breakIfRendering":{"category":"logic","description":"Cancel execution if the event is dispatched when the scene is rendering","fields":[],"name":"Break if rendering","version":"1.0.0"},"logic/ifElse":{"category":"logic","description":"If Condition Then Node Else Node","fields":[{"label":"Condition","name":"condition","required":true,"type":"boolean","value":"true"},{"label":"Truthy","name":"thenNode","type":"node"},{"label":"Falsy","name":"elseNode","type":"node"}],"name":"If-Else","settings":[],"version":"1.0.0"},"logic/nextSleepDuration":{"category":"logic","description":"Override the delay between renders","fields":[{"label":"Duration in seconds","name":"duration","required":true,"type":"float"}],"name":"Next sleep duration","version":"1.0.0"},"logic/setAsState":{"category":"logic","description":"Save the value (json node) as a state variable","fields":[{"label":"Value as a string","name":"valueString","required":false,"type":"string"},{"label":"Value as JSON","name":"valueJson","required":false,"type":"json"},{"label":"State key","name":"stateKey","placeholder":"","required":true,"type":"string","value":""},{"label":"Log to console","name":"debugLog","required":false,"type":"boolean","value":false}],"name":"Set as state","version":"1.0.0"},"render/calendar":{"category":"render","description":"Render a monthly calendar with events","fields":[{"label":"Background image","name":"inputImage","required":false,"type":"image","value":""},{"label":"Events JSON","name":"events","required":false,"type":"json","value":"[]"},{"label":"Year (0 = current)","name":"year","required":false,"type":"integer","value":"0"},{"label":"Month (1\u201312; 0 = current)","name":"month","required":false,"type":"integer","value":"0"},{"label":"Week starts on Monday","name":"startWeekOnMonday","required":true,"type":"boolean","value":"true"},{"label":"Scale (%) \u2014 scales fonts, paddings, strokes","name":"scale","required":true,"type":"integer","value":"100"},{"label":"Calendar theme","name":"theme","options":["light","dark","custom"],"required":true,"type":"select","value":"light"},{"label":"Transparent background","name":"transparentBackground","required":true,"type":"boolean","value":false},{"label":"Overall background color","name":"backgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"Weekend day background color (Sat/Sun cells)","name":"weekendBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#9eafd6"},{"label":"Today's cell outline color","name":"todayStrokeColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ff0000"},{"label":"Today's cell background color","name":"todayBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#e5afaf"},{"label":"Today's cell outline thickness","name":"todayStrokeWidth","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"2"},{"label":"Date number color","name":"dateTextColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"},{"label":"Event time color","name":"eventTimeColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#333333"},{"label":"Event title color","name":"eventTitleColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#333333"},{"label":"Title font (used for month & year)","name":"titleFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font"},{"label":"Month & year title font size","name":"titleFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"28"},{"label":"Title text color","name":"titleTextColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"},{"label":"Title background","name":"titleBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"Weekday font (used for weekday row)","name":"weekdayFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font"},{"label":"Weekday row font size","name":"weekdayFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"16"},{"label":"Weekday text color","name":"weekdayTextColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"},{"label":"Weekday background","name":"weekdayBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"Date number font","name":"dateFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font"},{"label":"Date number font size","name":"dateFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"18"},{"label":"Event title font","name":"eventTitleFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font","value":"Ubuntu-Medium.ttf"},{"label":"Event time font","name":"eventTimeFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font","value":"Ubuntu-Light.ttf"},{"label":"Event font size","name":"eventFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"14"},{"label":"Outer padding (px)","name":"padding","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"integer","value":"18"},{"label":"Show month & year title","name":"showMonthYear","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"boolean","value":"true"},{"label":"Title position (ignored if hidden)","name":"monthYearPosition","options":["top","bottom","none"],"required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"select","value":"top"},{"label":"Show grid lines","name":"showGrid","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"boolean","value":"true"},{"label":"Grid stroke width (px)","name":"gridWidth","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"1"},{"label":"Grid line color","name":"gridColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#999999"},{"label":"Show times for non all-day events","name":"showEventTimes","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"boolean","value":"true"},{"label":"All day event color count","name":"eventColorCount","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"integer","value":"1"},{"label":"All day event background #{color}","name":"eventColorBackground","seq":[["color",1,"eventColorCount"]],"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"All day event foreground #{color}","name":"eventColorForeground","seq":[["color",1,"eventColorCount"]],"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"}],"name":"Calendar","output":[{"name":"image","type":"image"}],"version":"1.3.0"},"render/color":{"category":"render","description":"Set a single color background","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Color","name":"color","required":true,"type":"color","value":"#ffffff"}],"name":"Color","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/gradient":{"category":"render","description":"Set a gradient background","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Start color (hex)","name":"startColor","required":true,"type":"color","value":"#800080"},{"label":"End color (hex)","name":"endColor","required":true,"type":"color","value":"#ffc0cb"},{"label":"Angle (degrees)","name":"angle","required":true,"type":"float","value":"45"}],"name":"Gradient","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/image":{"category":"render","description":"Render an image onto a canvas","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Image","name":"image","required":true,"type":"image"},{"label":"Placement","name":"placement","options":["cover","contain","stretch","center","tiled","top-left","top-center","top-right","center-left","center-right","bottom-left","bottom-center","bottom-right"],"required":true,"type":"select","value":"cover"},{"label":"Offset X","name":"offsetX","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Offset Y","name":"offsetY","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Blend Mode","name":"blendMode","options":["normal","overwrite","darken","multiply","color-burn","lighten","screen","color-dodge","overlay","soft-light","hard-light","difference","exclusion","hue","saturation","color","luminosity","mask","inverse-mask","exclude-mask"],"required":false,"type":"select","value":"normal"}],"name":"Render Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/opacity":{"category":"render","description":"Change how transparent an image is","fields":[{"label":"Image (optional)","name":"image","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image"},{"label":"Opacity (0-1)","name":"opacity","required":true,"type":"float","value":"1"}],"name":"Opacity","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/split":{"category":"render","description":"Render a grid","fields":[{"markdown":"Loop index in: `context.loopIndex`"},{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Rows","name":"rows","required":true,"type":"integer","value":"1"},{"label":"Columns","name":"columns","required":true,"type":"integer","value":"1"},{"label":"Hide empty cells","name":"hideEmpty","type":"boolean","value":"false"},{"label":"Render cell: {row} x {column}","name":"render_functions","seq":[["row",1,"rows"],["column",1,"columns"]],"showIf":[{"operator":"notEmpty"},{"field":"hideEmpty","operator":"eq","value":"false"}],"type":"node"},{"label":"Render all other cells","name":"render_function","type":"node"},{"label":"Gap","name":"gap","placeholder":"0","type":"string"},{"label":"Margin","name":"margin","placeholder":"0","type":"string"},{"hint":"Relative widths of columns, separated by spaces","label":"Widths","name":"width_ratios","placeholder":"1 2 1","type":"string"},{"hint":"Relative heights of rows, separated by spaces","label":"Heights","name":"height_ratios","placeholder":"1 2 1","type":"string"}],"name":"Split","output":[{"name":"image","type":"image"}],"settings":[],"version":"1.0.0"},"render/svg":{"category":"render","description":"Render an SVG onto a canvas","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"hint":"Provide raw SVG markup or a data URL like data:image/svg+xml;charset=utf-8,.","label":"SVG","name":"svg","required":true,"type":"text","value":""},{"label":"Placement","name":"placement","options":["cover","contain","stretch","center","tiled","top-left","top-center","top-right","center-left","center-right","bottom-left","bottom-center","bottom-right"],"required":true,"type":"select","value":"cover"},{"label":"Offset X","name":"offsetX","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Offset Y","name":"offsetY","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Blend Mode","name":"blendMode","options":["normal","overwrite","darken","multiply","color-burn","lighten","screen","color-dodge","overlay","soft-light","hard-light","difference","exclusion","hue","saturation","color","luminosity","mask","inverse-mask","exclude-mask"],"required":false,"type":"select","value":"normal"}],"name":"Render SVG","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/text":{"category":"render","description":"Overlay a block of text","fields":[{"label":"Image to print text on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Text","name":"text","placeholder":"Once upon a time...","required":false,"type":"text","value":""},{"hint":"Enable rich text editing\n\nThe \"basic-caret\" mode lets you change the size and color of each part of the text. Example syntax:\n- ^(16)font ^(32)size\n- ^(#FF00FF)color\n- ^(PTSans-Bold.ttf)font\n- ^(underline)lines ^(no-underline) not\n- ^(strikethrough)lines ^(no-strikethrough) not\n- ^(16,#FF0000) combine styles\n- Use ^(reset) to clean house","label":"Rich text mode","name":"richText","options":["disabled","basic-caret"],"required":false,"type":"select","value":"disabled"},{"label":"Align","name":"position","options":["left","center","right"],"placeholder":"center","required":true,"type":"select","value":"center"},{"label":"Align V","name":"vAlign","options":["top","middle","bottom"],"placeholder":"middle","required":true,"showIf":[{"field":".meta.showNextPrev"},{"and":[{"field":".meta.showOutput"},{"field":"inputImage"}]}],"type":"select","value":"middle"},{"label":"Offset X","name":"offsetX","placeholder":"0","required":true,"showIf":[{"field":".meta.showNextPrev"},{"field":"inputImage"}],"type":"float","value":"0"},{"label":"Offset Y","name":"offsetY","placeholder":"0","required":true,"showIf":[{"field":".meta.showNextPrev"},{"field":"inputImage"}],"type":"float","value":"0"},{"label":"Padding","name":"padding","placeholder":"10","required":true,"type":"float","value":"10"},{"label":"Font","name":"font","required":false,"type":"font"},{"label":"Font Color","name":"fontColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"},{"label":"Font Size","name":"fontSize","placeholder":"32","required":true,"type":"float","value":"32"},{"label":"Border Color","name":"borderColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Border width","name":"borderWidth","placeholder":"2","required":true,"type":"integer","value":"2"},{"label":"Overflow","name":"overflow","options":["fit-bounds","visible"],"type":"select","value":"fit-bounds"}],"name":"Text","output":[{"name":"image","type":"image"}],"version":"1.0.0"}}}""" +const appsJson* = """{"apps":{"data/beRecycle":{"cache":{"duration":"14400","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Return a JSON event list of trash pickup dates","fields":[{"label":"Street name","name":"streetName","placeholder":"","required":true,"type":"string","value":""},{"label":"Number","name":"number","placeholder":"","required":true,"type":"integer","value":""},{"label":"Postal code","name":"postalCode","placeholder":"","required":true,"type":"integer","value":""},{"label":"Export events from (YYYY-MM-DD)","name":"exportFrom","placeholder":"now","required":false,"type":"string","value":""},{"label":"Export events until (YYYY-MM-DD)","name":"exportUntil","placeholder":"1 year later","required":false,"type":"string","value":""},{"label":"Maximum number of events to export","name":"exportCount","placeholder":"50","required":false,"type":"integer","value":"50"},{"label":"Language","name":"language","options":["en","fr","nl"],"placeholder":"","required":true,"type":"select","value":"en"},{"hint":"In case the default value stops working, open recycleapp.be, and copy the value of the x-secret header sent to `/v1/access-token`.","label":"x-secret value","name":"xSecret","placeholder":"","required":false,"type":"string","value":""},{"markdown":"[{ summary, startTime, endTime, timezone }]"}],"name":"Recycling Calendar for Belgium","output":[{"name":"events","type":"json"}],"version":"1.0.0"},"data/chromiumScreenshot":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Capture a snapshot of a website with playwright and headless chromium. Needs a 64-bit system and at least 1GB of RAM.","fields":[{"label":"URL to capture","name":"url","placeholder":"","required":true,"type":"text","value":"https://frameos.net/"},{"label":"Width (0=full width)","name":"width","placeholder":"0","type":"integer","value":""},{"label":"Height (0=full height)","name":"height","placeholder":"0","type":"integer","value":""},{"hint":"When enabled, cookies/session storage persist between captures. Disable to use a fresh incognito-like session for each render.","label":"Persist browser session between renders","name":"persistSession","type":"boolean","value":true},{"hint":"Always install and run Chromium, even if we have less than 1GB of RAM. Disable this at your own risk!","label":"Disable low memory check","name":"disableLowMemoryCheck","type":"boolean","value":false}],"name":"Chromium screenshot","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/clock":{"category":"data","description":"Return the current time","fields":[{"markdown":"[Date format syntax](https://nim-lang.org/2.0.2/times.html)"},{"label":"Format","name":"format","options":["yyyy-MM-dd","yyyy-MM-dd HH:mm:ss","HH:mm:ss:fff","HH:mm:ss","HH:mm","custom"],"placeholder":"HH:mm:ss","required":true,"type":"select","value":"HH:mm:ss"},{"label":"Custom format","name":"formatCustom","placeholder":"","required":false,"showIf":[{"field":"format","operator":"eq","value":"custom"}],"type":"string","value":""}],"name":"Clock","output":[{"name":"time","type":"string"}],"version":"1.0.0"},"data/downloadImage":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Download image from an URL","fields":[{"label":"Image URL","name":"url","placeholder":"https://domain/image","required":true,"type":"text","value":""},{"hint":"Enter a state key to persist metadata about the image. Stores: {url, width, height, exif: {...} }","label":"Optional metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""}],"name":"Download Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/downloadUrl":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Download a URL into a string","fields":[{"label":"URL","name":"url","placeholder":"https://domain/content","required":true,"type":"text","value":""}],"name":"Download URL","output":[{"name":"result","type":"string"}],"version":"1.0.0"},"data/eventsToAgenda":{"category":"data","description":"Convert events to an basic-caret formatted agenda string","fields":[{"label":"Events","name":"events","required":true,"type":"json"},{"label":"Base font size","name":"baseFontSize","required":true,"type":"float","value":"24"},{"label":"Title font size","name":"titleFontSize","required":true,"type":"float","value":"48"},{"label":"Day title color","name":"titleColor","required":true,"type":"color","value":"#FFFFFF"},{"label":"Base text color","name":"textColor","required":true,"type":"color","value":"#FFFFFF"},{"label":"Time color","name":"timeColor","required":true,"type":"color","value":"#FF0000"},{"label":"Always start with today's date","name":"startWithToday","required":true,"type":"boolean","value":"true"}],"name":"Events to Agenda","output":[{"name":"result","type":"string"}],"version":"1.0.0"},"data/frameOSGallery":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Random image from the FrameOS gallery","fields":[{"markdown":"[Click here](https://gallery.frameos.net/) to see all the galleries."},{"label":"Category","name":"category","options":["building-art-styles","cute","cyberpunk-europe","masterpieces","space-gallery","space-odyssey","other"],"required":false,"type":"select","value":"cute"},{"label":"Category (if other)","name":"categoryOther","placeholder":"","required":false,"showIf":[{"field":"category","operator":"eq","value":"other"}],"type":"string","value":""}],"name":"FrameOS Gallery","output":[{"name":"image","type":"image"}],"settings":["frameOS"],"version":"1.0.0"},"data/haSensor":{"cache":{"duration":"60","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Get the state of a Home Assistant entity","fields":[{"markdown":"Find the [entity id here](http://homeassistant.local:8123/config/entities)"},{"label":"Entity ID","name":"entityId","placeholder":"Home Assistant entity name. Example: sensor.home_solar_percentage or water_heater.hot_water","required":true,"type":"text"},{"label":"Debug logging","name":"debug","required":false,"type":"boolean","value":"false"}],"name":"Home Assistant Sensor","output":[{"name":"state","type":"json"}],"settings":["homeAssistant"],"version":"1.0.0"},"data/icalJson":{"cache":{"enabled":true,"inputEnabled":true},"category":"data","description":"Convert an iCal file into a JSON event list","fields":[{"label":"iCal file contents","name":"ical","required":true,"type":"string"},{"label":"Export events from (YYYY-MM-DD)","name":"exportFrom","placeholder":"now","required":false,"type":"string","value":""},{"label":"Export events until (YYYY-MM-DD)","name":"exportUntil","placeholder":"1 year later","required":false,"type":"string","value":""},{"label":"Maximum number of events to export","name":"exportCount","placeholder":"50","required":false,"type":"integer","value":"50"},{"label":"Filter events by keyword","name":"search","placeholder":"","required":false,"type":"string","value":""},{"label":"Add 'location' to the result JSON","name":"addLocation","required":false,"type":"boolean","value":"true"},{"label":"Add 'url' to the result JSON","name":"addUrl","required":false,"type":"boolean","value":"true"},{"label":"Add 'description' to the result JSON","name":"addDescription","required":false,"type":"boolean","value":"false"},{"label":"Add 'timezone' to the result JSON","name":"addTimezone","required":false,"type":"boolean","value":"false"},{"markdown":"[{ summary, startTime, endTime, location, url, description, timezone }]"}],"name":"iCal to Events JSON","output":[{"name":"events","type":"json"}],"version":"1.0.0"},"data/localImage":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Show an image from the SD card","fields":[{"label":"Image filename or folder","name":"path","placeholder":"Defaults to the assets folder (e.g. /srv/assets)","required":true,"type":"text","value":""},{"label":"Order of images","name":"order","options":["random","alphabetical"],"required":true,"type":"select","value":"random"},{"hint":"Enter a state key to persist the current image index between restarts.","label":"Optional state key for persistence","name":"counterStateKey","required":false,"type":"string","value":""},{"hint":"Enter a state key to persist metadata about the last image. Stores: {path, filename, index, total, width, height, exif: {...} }","label":"Optional metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""},{"label":"Filter filenames by keyword","name":"search","required":false,"type":"string","value":""}],"name":"Local Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/log":{"category":"data","description":"Log a JSON object","fields":[{"label":"Input JSON","name":"inputJson","required":false,"type":"json"}],"name":"Log JSON","output":[{"name":"outputJson","type":"json"}],"version":"1.0.0"},"data/newImage":{"cache":{"enabled":true,"inputEnabled":true},"category":"data","description":"Create a new image with a single color background","fields":[{"label":"Width","name":"width","placeholder":"auto","required":false,"type":"integer"},{"label":"Height","name":"height","placeholder":"auto","required":false,"type":"integer"},{"label":"Color","name":"color","required":true,"type":"color","value":"#ffffff"},{"label":"Opacity (0-1)","name":"opacity","required":false,"type":"float","value":"1"},{"label":"Render next","name":"renderNext","required":false,"type":"node"}],"name":"New Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/openaiImage":{"cache":{"duration":"3600","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Random AI generated art from OpenAI's image models","fields":[{"label":"Prompt","name":"prompt","placeholder":"e.g. pumpkin pyjama party, digital art","required":true,"rows":4,"type":"text","value":""},{"label":"Model","name":"model","options":["gpt-image-2","gpt-image-1.5","gpt-image-1","dall-e-3","dall-e-2"],"required":true,"type":"select","value":"gpt-image-2"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","1536x1024","1024x1536"],"showIf":[{"field":"model","operator":"eq","value":"gpt-image-2"},{"field":"model","operator":"eq","value":"gpt-image-1"},{"field":"model","operator":"eq","value":"gpt-image-1.5"}],"type":"select","value":"best for orientation"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","1792x1024","1024x1792"],"showIf":[{"field":"model","operator":"eq","value":"dall-e-3"}],"type":"select","value":"best for orientation"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","512x512","256x256"],"showIf":[{"field":"model","operator":"eq","value":"dall-e-2"}],"type":"select","value":"best for orientation"},{"label":"Style","name":"style","options":["vivid","natural",""],"showIf":[{"field":"model","operator":"eq","value":"dall-e-3"}],"type":"select","value":"vivid"},{"label":"Quality","name":"quality","options":["standard","hd",""],"showIf":[{"field":"model","operator":"eq","value":"dall-e-3"}],"type":"select","value":"standard"},{"hint":"Enter a state key to persist metadata about the image. Stores: {prompt, model, size}.","label":"Metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""},{"hint":"Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\n\nYou can later use the 'Local image' app to view saved assets.\n\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved.","label":"Save asset","name":"saveAssets","options":["auto","always","never"],"type":"select","value":"auto"}],"name":"OpenAI Image","output":[{"name":"image","type":"image"}],"settings":["openAI"],"version":"1.0.0"},"data/openaiText":{"cache":{"duration":"600","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Text response from ChatGPT and friends","fields":[{"label":"System Prompt","name":"system","placeholder":"","required":true,"rows":6,"type":"text","value":"You're a smart e-ink frame running FrameOS. Reply with plain text only. Space is very limited."},{"label":"User Prompt","name":"user","placeholder":"Write your prompt here. Keep it short and clear.","required":true,"rows":6,"type":"text","value":""},{"label":"Model","name":"model","required":true,"type":"string","value":"gpt-5.5"}],"name":"OpenAI Text","output":[{"name":"reply","type":"string"}],"settings":["openAI"],"version":"1.0.0"},"data/parseJson":{"category":"data","description":"Parse a text string and convert to JSON","fields":[{"label":"Text","name":"text","required":true,"type":"text"}],"name":"Parse JSON","output":[{"name":"json","type":"json"}],"version":"1.0.0"},"data/prettyJson":{"category":"data","description":"Pretty print JSON into a string","fields":[{"label":"JSON","name":"json","required":true,"type":"json"},{"label":"Indent","name":"ident","required":false,"type":"integer","value":"2"},{"label":"Prettify","name":"prettify","required":false,"type":"boolean","value":"true"}],"name":"Pretty JSON","output":[{"name":"result","type":"string"}],"version":"1.0.0"},"data/qr":{"cache":{"enabled":true,"inputEnabled":true},"category":"data","description":"QR codes. Default to a link to the frame control URL.","fields":[{"label":"Code Type","name":"codeType","options":["Frame Control URL","Frame Image URL","Custom"],"required":false,"type":"select","value":"Frame Control URL"},{"label":"Custom code","name":"code","required":false,"showIf":[{"field":"codeType","operator":"eq","value":"Custom"}],"type":"string","value":""},{"label":"Size","name":"size","type":"float","value":"2"},{"label":"Size unit","name":"sizeUnit","options":["pixels per dot","pixels total","percent"],"required":true,"type":"select","value":"pixels per dot"},{"label":"Alignment pattern radius %","name":"alRad","type":"float","value":"30"},{"label":"Module radius %","name":"moRad","type":"float","value":"0"},{"label":"Module separation %","name":"moSep","type":"float","value":"0"},{"label":"Padding in dots","name":"padding","placeholder":"1","required":true,"type":"integer","value":"1"},{"label":"QR Code Color","name":"qrCodeColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Background Color","name":"backgroundColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"}],"name":"QR Code","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/resizeImage":{"category":"data","description":"Scale or stretch an image","fields":[{"label":"Image","name":"image","required":true,"type":"image"},{"label":"New Width","name":"width","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"New Height","name":"height","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"contain"}],"name":"Resize","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/rotateImage":{"category":"data","description":"Rotate an image","fields":[{"label":"Image","name":"image","required":true,"type":"image"},{"label":"Rotation Degree","name":"rotationDegree","placeholder":"e.g., 45","required":true,"type":"float","value":"0"}],"name":"Rotate","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/rstpSnapshot":{"apt":["ffmpeg"],"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Take a still image from a webcam feed with ffmpeg","fields":[{"label":"RTSP URL","name":"url","placeholder":"rstp://domain/cam","required":true,"type":"text","value":""}],"name":"RSTP Snapshot","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/unsplash":{"cache":{"duration":"900","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Random unsplash image","fields":[{"label":"Search","name":"search","placeholder":"e.g. pineapple, nature, birds, power","required":false,"type":"string","value":"nature"},{"label":"Orientation","name":"orientation","options":["auto","any","landscape","portrait","squarish"],"placeholder":"landscape, portrait, square","required":false,"type":"select","value":"auto"},{"hint":"Enter a state key to persist metadata about the image. Stores: {search, description, author, ...}.","label":"Metadata state key","name":"metadataStateKey","required":false,"type":"string","value":""},{"hint":"Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\n\nYou can later use the 'Local image' app to view saved assets.\n\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved.","label":"Save asset","name":"saveAssets","options":["auto","always","never"],"type":"select","value":"auto"}],"name":"Unsplash","output":[{"name":"image","type":"image"}],"settings":["unsplash"],"version":"1.0.0"},"data/weather":{"category":"data","description":"Fetch current, hourly, and daily forecasts from Open-Meteo for a location and date.","fields":[{"label":"Location","name":"location","placeholder":"City, state, or country","required":true,"type":"string","value":""},{"label":"Date (YYYY-MM-DD)","name":"date","placeholder":"2024-01-30","required":false,"type":"string","value":""},{"label":"Timezone","name":"timezone","placeholder":"auto","required":false,"type":"string","value":"auto"},{"label":"Temperature unit","name":"temperatureUnit","options":["celsius","fahrenheit"],"required":true,"type":"select","value":"celsius"},{"label":"Wind speed unit","name":"windSpeedUnit","options":["kmh","ms","mph","kn"],"required":true,"type":"select","value":"kmh"},{"label":"Precipitation unit","name":"precipitationUnit","options":["mm","inch"],"required":true,"type":"select","value":"mm"}],"name":"Weather (Open-Meteo)","output":[{"example":"{\n \"provider\": \"open-meteo\",\n \"forecastModes\": [\n \"current\",\n \"hourly\",\n \"daily\"\n ],\n \"date\": \"2026-01-18\",\n \"location\": {\n \"name\": \"Brussels\",\n \"latitude\": 50.85045,\n \"longitude\": 4.34878,\n \"timezone\": \"auto\",\n \"country\": \"Belgium\",\n \"countryCode\": \"BE\",\n \"admin1\": \"Brussels Capital\",\n \"admin2\": \"Bruxelles-Capitale\"\n },\n \"forecast\": {\n \"latitude\": 50.854,\n \"longitude\": 4.35,\n \"generationtime_ms\": 0.3679990768432617,\n \"utc_offset_seconds\": 3600,\n \"timezone\": \"Europe/Brussels\",\n \"timezone_abbreviation\": \"GMT+1\",\n \"elevation\": 27,\n \"current_weather_units\": {\n \"time\": \"iso8601\",\n \"interval\": \"seconds\",\n \"temperature\": \"\\u00b0C\",\n \"windspeed\": \"km/h\",\n \"winddirection\": \"\\u00b0\",\n \"is_day\": \"\",\n \"weathercode\": \"wmo code\"\n },\n \"current_weather\": {\n \"time\": \"2026-01-18T22:15\",\n \"interval\": 900,\n \"temperature\": 5.7,\n \"windspeed\": 5.4,\n \"winddirection\": 132,\n \"is_day\": 0,\n \"weathercode\": 0\n },\n \"hourly_units\": {\n \"time\": \"iso8601\",\n \"temperature_2m\": \"\\u00b0C\",\n \"apparent_temperature\": \"\\u00b0C\",\n \"precipitation\": \"mm\",\n \"weathercode\": \"wmo code\",\n \"windspeed_10m\": \"km/h\",\n \"winddirection_10m\": \"\\u00b0\"\n },\n \"hourly\": {\n \"time\": [\n \"2026-01-18T00:00\",\n \"2026-01-18T01:00\",\n \"2026-01-18T02:00\",\n \"2026-01-18T03:00\",\n \"2026-01-18T04:00\",\n \"2026-01-18T05:00\",\n \"2026-01-18T06:00\",\n \"2026-01-18T07:00\",\n \"2026-01-18T08:00\",\n \"2026-01-18T09:00\",\n \"2026-01-18T10:00\",\n \"2026-01-18T11:00\",\n \"2026-01-18T12:00\",\n \"2026-01-18T13:00\",\n \"2026-01-18T14:00\",\n \"2026-01-18T15:00\",\n \"2026-01-18T16:00\",\n \"2026-01-18T17:00\",\n \"2026-01-18T18:00\",\n \"2026-01-18T19:00\",\n \"2026-01-18T20:00\",\n \"2026-01-18T21:00\",\n \"2026-01-18T22:00\",\n \"2026-01-18T23:00\"\n ],\n \"temperature_2m\": [\n 9.2,\n 8.2,\n 8.3,\n 8.1,\n 7.2,\n 7.1,\n 7.3,\n 7.1,\n 6.7,\n 7.3,\n 8.5,\n 8.5,\n 9.7,\n 10.9,\n 11.1,\n 11.3,\n 11.1,\n 9.7,\n 8.7,\n 8,\n 6.8,\n 6.2,\n 5.8,\n 5.5\n ],\n \"apparent_temperature\": [\n 7.5,\n 6.3,\n 6.5,\n 6.2,\n 5.1,\n 4.8,\n 4.9,\n 4.8,\n 3.7,\n 4.4,\n 5.6,\n 5.6,\n 6.9,\n 8.7,\n 8,\n 8.6,\n 8.3,\n 6.7,\n 5.9,\n 5,\n 3.6,\n 3.5,\n 3.3,\n 2.4\n ],\n \"precipitation\": [\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0,\n 0\n ],\n \"weathercode\": [\n 0,\n 1,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 3,\n 0,\n 0,\n 1,\n 2,\n 2,\n 0,\n 3,\n 3,\n 2,\n 0,\n 0,\n 0,\n 0\n ],\n \"windspeed_10m\": [\n 4,\n 3.6,\n 2.9,\n 3.6,\n 5.8,\n 5,\n 6.5,\n 5,\n 9.4,\n 9,\n 8.6,\n 9,\n 9.4,\n 5.8,\n 9.7,\n 7.9,\n 8.6,\n 9.4,\n 10.4,\n 8.6,\n 9.7,\n 5.8,\n 5,\n 7.2\n ],\n \"winddirection_10m\": [\n 166,\n 59,\n 353,\n 67,\n 78,\n 52,\n 71,\n 68,\n 101,\n 54,\n 87,\n 80,\n 62,\n 66,\n 69,\n 74,\n 92,\n 73,\n 77,\n 110,\n 117,\n 202,\n 132,\n 168\n ]\n },\n \"daily_units\": {\n \"time\": \"iso8601\",\n \"temperature_2m_max\": \"\\u00b0C\",\n \"temperature_2m_min\": \"\\u00b0C\",\n \"precipitation_sum\": \"mm\",\n \"weathercode\": \"wmo code\",\n \"sunrise\": \"iso8601\",\n \"sunset\": \"iso8601\",\n \"windspeed_10m_max\": \"km/h\"\n },\n \"daily\": {\n \"time\": [\n \"2026-01-18\"\n ],\n \"temperature_2m_max\": [\n 11.3\n ],\n \"temperature_2m_min\": [\n 5.5\n ],\n \"precipitation_sum\": [\n 0\n ],\n \"weathercode\": [\n 3\n ],\n \"sunrise\": [\n \"2026-01-18T08:35\"\n ],\n \"sunset\": [\n \"2026-01-18T17:10\"\n ],\n \"windspeed_10m_max\": [\n 10.4\n ]\n }\n }\n}","name":"weather","type":"json"}],"version":"1.0.0"},"data/wikicommons":{"cache":{"duration":"3600","durationEnabled":true,"enabled":true,"inputEnabled":true},"category":"data","description":"Images from Wikimedia Commons","fields":[{"hint":"Choose today's Picture of the Day, the Picture of the Day from this date in a previous year, a random previous Picture of the Day, or a random Commons image.","label":"Mode","name":"mode","options":["pictureOfTheDay","onThisDay","randomPictureOfTheDay","randomImage"],"required":false,"type":"select","value":"pictureOfTheDay"},{"hint":"Save the generated image to disk as an asset. It'll be placed into the frame's assets folder.\n\nYou can later use the 'Local image' app to view saved assets.\n\nIf set to 'auto', the image will be saved if the frame is set to save assets. If set to 'always', the image will always be saved. If set to 'never', the image will never be saved.","label":"Save asset","name":"saveAssets","options":["auto","always","never"],"type":"select","value":"auto"},{"label":"Metadata state key","name":"metadataStateKey","placeholder":"e.g. wikimediaMetadata","required":false,"type":"string","value":""}],"name":"Wikimedia Commons","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"data/xmlToJson":{"category":"data","description":"Convert XML into a JSON tree using Nim's xmlparser","fields":[{"label":"XML","name":"xml","required":true,"type":"string"}],"name":"XML to JSON","output":[{"name":"json","type":"json"}],"version":"1.0.0"},"legacy/clock":{"category":"legacy","description":"Overlay current time on the image","fields":[{"markdown":"[Date format syntax](https://nim-lang.org/2.0.2/times.html)"},{"label":"Format","name":"format","options":["yyyy-MM-dd","yyyy-MM-dd HH:mm:ss","HH:mm:ss:fff","HH:mm:ss","HH:mm","custom"],"placeholder":"HH:mm:ss","required":true,"type":"select","value":"HH:mm:ss"},{"label":"Custom format","name":"formatCustom","placeholder":"","required":false,"type":"string","value":""},{"label":"Position","name":"position","options":["top-left","top-center","top-right","center-left","center-center","center-right","bottom-left","bottom-center","bottom-right"],"placeholder":"center-center","required":true,"type":"select","value":"center-center"},{"label":"Offset X","name":"offsetX","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Offset Y","name":"offsetY","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Padding","name":"padding","placeholder":"10","required":true,"type":"float","value":"10"},{"label":"Font Color","name":"fontColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"},{"label":"Font Size","name":"fontSize","placeholder":"32","required":true,"type":"float","value":"32"},{"label":"Border Color","name":"borderColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Border width","name":"borderWidth","placeholder":"2","required":true,"type":"integer","value":"2"}],"name":"Clock (legacy)","version":"1.0.0"},"legacy/downloadImage":{"category":"legacy","description":"Download image from an URL","fields":[{"label":"Image URL","name":"url","placeholder":"https://domain/image","required":true,"type":"text","value":""},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"Download Image (legacy)","version":"1.0.0"},"legacy/frameOSGallery":{"category":"legacy","description":"Random image from the FrameOS gallery","fields":[{"markdown":"[Click here](https://gallery.frameos.net/) to see all the galleries."},{"label":"Category","name":"category","options":["building-art-styles","cute","cyberpunk-europe","masterpieces","space-gallery","space-odyssey"],"required":false,"type":"select","value":"cute"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"FrameOS Gallery (legacy)","settings":["frameOS"],"version":"1.0.0"},"legacy/haSensor":{"category":"legacy","description":"Store the state of a Home Assistant entity in the scene's state","fields":[{"markdown":"Find the [entity id here](http://homeassistant.local:8123/config/entities). Then use code like:\n\nscene.state{\"water_heater\"}{\"state\"}.getStr"},{"label":"Entity ID","name":"entityId","placeholder":"Home Assistant entity name. Example: sensor.home_solar_percentage or water_heater.hot_water","required":true,"type":"text"},{"label":"State key to store the json in","name":"stateKey","placeholder":"","required":true,"type":"text","value":"sensor"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 60. Use 0 for no cache","required":false,"type":"float","value":"60"},{"label":"Debug logging","name":"debug","required":false,"type":"boolean","value":"false"}],"name":"HA Sensor (legacy)","settings":["homeAssistant"],"version":"1.0.0"},"legacy/localImage":{"category":"legacy","description":"Show an image from the SD card","fields":[{"label":"Image filename or folder","name":"path","placeholder":"/srv/images","required":true,"type":"text","value":"/srv/images"},{"label":"Order of images","name":"order","options":["random","alphabetical"],"required":true,"type":"select","value":"random"},{"label":"Seconds to show one image","name":"seconds","placeholder":"Default: 900 (15min)","required":false,"type":"float","value":"900"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Optional state key for persistence","name":"counterStateKey","required":false,"type":"string","value":""}],"name":"Local Image (legacy)","version":"1.0.0"},"legacy/openai":{"category":"legacy","description":"Random AI generated art from OpenAI's image models","fields":[{"label":"Prompt","name":"prompt","placeholder":"e.g. pumpkin pyjama party, digital art","required":true,"rows":6,"type":"text","value":""},{"label":"Model","name":"model","options":["gpt-image-2","gpt-image-1.5","gpt-image-1","dall-e-3","dall-e-2"],"required":true,"type":"select","value":"gpt-image-2"},{"label":"Size","name":"size","options":["best for orientation","1024x1024","1536x1024","1024x1536"],"type":"select","value":"best for orientation"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"},{"label":"Style","name":"style","options":["vivid","natural",""],"type":"select","value":"vivid"},{"label":"Quality","name":"quality","options":["standard","hd",""],"type":"select","value":"standard"},{"label":"Seconds to cache each prompt","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"OpenAI Image (legacy)","settings":["openAI"],"version":"1.0.0"},"legacy/openaiText":{"category":"legacy","description":"Text response from ChatGPT and friends","fields":[{"label":"System Prompt","name":"system","placeholder":"","required":true,"rows":6,"type":"text","value":"You're a smart e-ink frame running FrameOS. Reply with plain text only. Space is very limited."},{"label":"User Prompt","name":"user","placeholder":"Write your prompt here. Keep it short and clear.","required":true,"rows":6,"type":"text","value":""},{"label":"Model","name":"model","required":true,"type":"string","value":"gpt-5.5"},{"label":"State key for reply","name":"stateKey","required":true,"type":"string","value":"reply"},{"label":"Seconds to cache each prompt","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 for no cache","required":false,"type":"float","value":"3600"}],"name":"OpenAI Text (legacy)","settings":["openAI"],"version":"1.0.0"},"legacy/qr":{"category":"legacy","description":"Display QR codes. Default to link to self.","fields":[{"label":"Code Type","name":"codeType","options":["Frame Control URL","Frame Image URL","Custom"],"required":false,"type":"select","value":"Frame Control URL"},{"label":"Code (if Custom above)","name":"code","required":false,"type":"string","value":""},{"label":"Size","name":"size","type":"float","value":"2"},{"label":"Size unit","name":"sizeUnit","options":["percent","pixels per dot","pixels total"],"required":true,"type":"select","value":"pixels per dot"},{"label":"Alignment pattern radius %","name":"alRad","type":"float","value":"30"},{"label":"Module radius %","name":"moRad","type":"float","value":"0"},{"label":"Module separation %","name":"moSep","type":"float","value":"0"},{"label":"Position","name":"position","options":["top-left","top-center","top-right","center-left","center-center","center-right","bottom-left","bottom-center","bottom-right"],"placeholder":"center-center","required":true,"type":"select","value":"center-center"},{"label":"Offset X","name":"offsetX","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Offset Y","name":"offsetY","placeholder":"0","required":true,"type":"float","value":"0"},{"label":"Padding in dots","name":"padding","placeholder":"1","required":true,"type":"integer","value":"1"},{"label":"QR Code Color","name":"qrCodeColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Background Color","name":"backgroundColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"}],"name":"QR Code (legacy)","version":"1.0.0"},"legacy/resize":{"category":"legacy","description":"Scale or stretch the image","fields":[{"label":"New Width","name":"width","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"New Height","name":"height","placeholder":"e.g., 1024","required":true,"type":"integer"},{"label":"Scaling mode","name":"scalingMode","options":["cover","contain","stretch","center"],"required":true,"type":"select","value":"contain"}],"name":"Resize (legacy)","version":"1.0.0"},"legacy/rotate":{"category":"legacy","description":"Rotate the image","fields":[{"label":"Rotation Degree","name":"rotationDegree","placeholder":"e.g., 45","required":true,"type":"float","value":"0"},{"label":"Scaling mode","name":"scalingMode","options":["expand","cover","contain","stretch","center"],"required":true,"type":"select","value":"cover"}],"name":"Rotate (legacy)","version":"1.0.0"},"legacy/unsplash":{"category":"legacy","description":"Random unsplash image","fields":[{"label":"Random keyword (one word)","name":"keyword","placeholder":"e.g. pineapple, nature, birds, power","required":false,"type":"string","value":"nature"},{"label":"Seconds to cache the result","name":"cacheSeconds","placeholder":"Default: 3600 (1h). Use 0 to refetch on every render.","required":false,"type":"float","value":"3600"}],"name":"Unsplash (legacy/broken)","version":"1.0.0"},"logic/breakIfRendering":{"category":"logic","description":"Cancel execution if the event is dispatched when the scene is rendering","fields":[],"name":"Break if rendering","version":"1.0.0"},"logic/ifElse":{"category":"logic","description":"If Condition Then Node Else Node","fields":[{"label":"Condition","name":"condition","required":true,"type":"boolean","value":"true"},{"label":"Truthy","name":"thenNode","type":"node"},{"label":"Falsy","name":"elseNode","type":"node"}],"name":"If-Else","settings":[],"version":"1.0.0"},"logic/nextSleepDuration":{"category":"logic","description":"Override the delay between renders","fields":[{"label":"Duration in seconds","name":"duration","required":true,"type":"float"}],"name":"Next sleep duration","version":"1.0.0"},"logic/setAsState":{"category":"logic","description":"Save the value (json node) as a state variable","fields":[{"label":"Value as a string","name":"valueString","required":false,"type":"string"},{"label":"Value as JSON","name":"valueJson","required":false,"type":"json"},{"label":"State key","name":"stateKey","placeholder":"","required":true,"type":"string","value":""},{"label":"Log to console","name":"debugLog","required":false,"type":"boolean","value":false}],"name":"Set as state","version":"1.0.0"},"render/calendar":{"category":"render","description":"Render a monthly calendar with events","fields":[{"label":"Background image","name":"inputImage","required":false,"type":"image","value":""},{"label":"Events JSON","name":"events","required":false,"type":"json","value":"[]"},{"label":"Year (0 = current)","name":"year","required":false,"type":"integer","value":"0"},{"label":"Month (1\u201312; 0 = current)","name":"month","required":false,"type":"integer","value":"0"},{"label":"Week starts on Monday","name":"startWeekOnMonday","required":true,"type":"boolean","value":"true"},{"label":"Scale (%) \u2014 scales fonts, paddings, strokes","name":"scale","required":true,"type":"integer","value":"100"},{"label":"Calendar theme","name":"theme","options":["light","dark","custom"],"required":true,"type":"select","value":"light"},{"label":"Transparent background","name":"transparentBackground","required":true,"type":"boolean","value":false},{"label":"Overall background color","name":"backgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"Weekend day background color (Sat/Sun cells)","name":"weekendBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#9eafd6"},{"label":"Today's cell outline color","name":"todayStrokeColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ff0000"},{"label":"Today's cell background color","name":"todayBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#e5afaf"},{"label":"Today's cell outline thickness","name":"todayStrokeWidth","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"2"},{"label":"Date number color","name":"dateTextColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"},{"label":"Event time color","name":"eventTimeColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#333333"},{"label":"Event title color","name":"eventTitleColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#333333"},{"label":"Title font (used for month & year)","name":"titleFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font"},{"label":"Month & year title font size","name":"titleFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"28"},{"label":"Title text color","name":"titleTextColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"},{"label":"Title background","name":"titleBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"Weekday font (used for weekday row)","name":"weekdayFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font"},{"label":"Weekday row font size","name":"weekdayFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"16"},{"label":"Weekday text color","name":"weekdayTextColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"},{"label":"Weekday background","name":"weekdayBackgroundColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"Date number font","name":"dateFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font"},{"label":"Date number font size","name":"dateFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"18"},{"label":"Event title font","name":"eventTitleFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font","value":"Ubuntu-Medium.ttf"},{"label":"Event time font","name":"eventTimeFont","required":false,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"font","value":"Ubuntu-Light.ttf"},{"label":"Event font size","name":"eventFontSize","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"14"},{"label":"Outer padding (px)","name":"padding","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"integer","value":"18"},{"label":"Show month & year title","name":"showMonthYear","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"boolean","value":"true"},{"label":"Title position (ignored if hidden)","name":"monthYearPosition","options":["top","bottom","none"],"required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"select","value":"top"},{"label":"Show grid lines","name":"showGrid","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"boolean","value":"true"},{"label":"Grid stroke width (px)","name":"gridWidth","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"float","value":"1"},{"label":"Grid line color","name":"gridColor","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#999999"},{"label":"Show times for non all-day events","name":"showEventTimes","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"boolean","value":"true"},{"label":"All day event color count","name":"eventColorCount","required":true,"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"integer","value":"1"},{"label":"All day event background #{color}","name":"eventColorBackground","seq":[["color",1,"eventColorCount"]],"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#ffffff"},{"label":"All day event foreground #{color}","name":"eventColorForeground","seq":[["color",1,"eventColorCount"]],"showIf":[{"field":"theme","operator":"eq","value":"custom"}],"type":"color","value":"#000000"}],"name":"Calendar","output":[{"name":"image","type":"image"}],"version":"1.3.0"},"render/color":{"category":"render","description":"Set a single color background","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Color","name":"color","required":true,"type":"color","value":"#ffffff"}],"name":"Color","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/gradient":{"category":"render","description":"Set a gradient background","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Start color (hex)","name":"startColor","required":true,"type":"color","value":"#800080"},{"label":"End color (hex)","name":"endColor","required":true,"type":"color","value":"#ffc0cb"},{"label":"Angle (degrees)","name":"angle","required":true,"type":"float","value":"45"}],"name":"Gradient","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/image":{"category":"render","description":"Render an image onto a canvas","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Image","name":"image","required":true,"type":"image"},{"label":"Placement","name":"placement","options":["cover","contain","stretch","center","tiled","top-left","top-center","top-right","center-left","center-right","bottom-left","bottom-center","bottom-right"],"required":true,"type":"select","value":"cover"},{"label":"Offset X","name":"offsetX","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Offset Y","name":"offsetY","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Blend Mode","name":"blendMode","options":["normal","overwrite","darken","multiply","color-burn","lighten","screen","color-dodge","overlay","soft-light","hard-light","difference","exclusion","hue","saturation","color","luminosity","mask","inverse-mask","exclude-mask"],"required":false,"type":"select","value":"normal"}],"name":"Render Image","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/opacity":{"category":"render","description":"Change how transparent an image is","fields":[{"label":"Image (optional)","name":"image","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image"},{"label":"Opacity (0-1)","name":"opacity","required":true,"type":"float","value":"1"}],"name":"Opacity","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/split":{"category":"render","description":"Render a grid","fields":[{"markdown":"Loop index in: `context.loopIndex`"},{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Rows","name":"rows","required":true,"type":"integer","value":"1"},{"label":"Columns","name":"columns","required":true,"type":"integer","value":"1"},{"label":"Hide empty cells","name":"hideEmpty","type":"boolean","value":"false"},{"label":"Render cell: {row} x {column}","name":"render_functions","seq":[["row",1,"rows"],["column",1,"columns"]],"showIf":[{"operator":"notEmpty"},{"field":"hideEmpty","operator":"eq","value":"false"}],"type":"node"},{"label":"Render all other cells","name":"render_function","type":"node"},{"label":"Gap","name":"gap","placeholder":"0","type":"string"},{"label":"Margin","name":"margin","placeholder":"0","type":"string"},{"hint":"Relative widths of columns, separated by spaces","label":"Widths","name":"width_ratios","placeholder":"1 2 1","type":"string"},{"hint":"Relative heights of rows, separated by spaces","label":"Heights","name":"height_ratios","placeholder":"1 2 1","type":"string"}],"name":"Split","output":[{"name":"image","type":"image"}],"settings":[],"version":"1.0.0"},"render/svg":{"category":"render","description":"Render an SVG onto a canvas","fields":[{"label":"Image to render on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"hint":"Provide raw SVG markup or a data URL like data:image/svg+xml;charset=utf-8,.","label":"SVG","name":"svg","required":true,"type":"text","value":""},{"label":"Placement","name":"placement","options":["cover","contain","stretch","center","tiled","top-left","top-center","top-right","center-left","center-right","bottom-left","bottom-center","bottom-right"],"required":true,"type":"select","value":"cover"},{"label":"Offset X","name":"offsetX","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Offset Y","name":"offsetY","required":false,"showIf":[{"field":"placement","operator":"notIn","value":["cover","contain","stretch"]}],"type":"integer","value":"0"},{"label":"Blend Mode","name":"blendMode","options":["normal","overwrite","darken","multiply","color-burn","lighten","screen","color-dodge","overlay","soft-light","hard-light","difference","exclusion","hue","saturation","color","luminosity","mask","inverse-mask","exclude-mask"],"required":false,"type":"select","value":"normal"}],"name":"Render SVG","output":[{"name":"image","type":"image"}],"version":"1.0.0"},"render/text":{"category":"render","description":"Overlay a block of text","fields":[{"label":"Image to print text on (optional)","name":"inputImage","required":false,"showIf":[{"field":".meta.showOutput"}],"type":"image","value":""},{"label":"Text","name":"text","placeholder":"Once upon a time...","required":false,"type":"text","value":""},{"hint":"Enable rich text editing\n\nThe \"basic-caret\" mode lets you change the size and color of each part of the text. Example syntax:\n- ^(16)font ^(32)size\n- ^(#FF00FF)color\n- ^(PTSans-Bold.ttf)font\n- ^(underline)lines ^(no-underline) not\n- ^(strikethrough)lines ^(no-strikethrough) not\n- ^(16,#FF0000) combine styles\n- Use ^(reset) to clean house","label":"Rich text mode","name":"richText","options":["disabled","basic-caret"],"required":false,"type":"select","value":"disabled"},{"label":"Align","name":"position","options":["left","center","right"],"placeholder":"center","required":true,"type":"select","value":"center"},{"label":"Align V","name":"vAlign","options":["top","middle","bottom"],"placeholder":"middle","required":true,"showIf":[{"field":".meta.showNextPrev"},{"and":[{"field":".meta.showOutput"},{"field":"inputImage"}]}],"type":"select","value":"middle"},{"label":"Offset X","name":"offsetX","placeholder":"0","required":true,"showIf":[{"field":".meta.showNextPrev"},{"field":"inputImage"}],"type":"float","value":"0"},{"label":"Offset Y","name":"offsetY","placeholder":"0","required":true,"showIf":[{"field":".meta.showNextPrev"},{"field":"inputImage"}],"type":"float","value":"0"},{"label":"Padding","name":"padding","placeholder":"10","required":true,"type":"float","value":"10"},{"label":"Font","name":"font","required":false,"type":"font"},{"label":"Font Color","name":"fontColor","placeholder":"#ffffff","required":true,"type":"color","value":"#ffffff"},{"label":"Font Size","name":"fontSize","placeholder":"32","required":true,"type":"float","value":"32"},{"label":"Border Color","name":"borderColor","placeholder":"#000000","required":true,"type":"color","value":"#000000"},{"label":"Border width","name":"borderWidth","placeholder":"2","required":true,"type":"integer","value":"2"},{"label":"Overflow","name":"overflow","options":["fit-bounds","visible"],"type":"select","value":"fit-bounds"}],"name":"Text","output":[{"name":"image","type":"image"}],"version":"1.0.0"}}}""" proc getAppsJson*(): string = appsJson diff --git a/repo/scenes/samples/Wikimedia Commons/image.jpg b/repo/scenes/samples/Wikimedia Commons/image.jpg new file mode 100644 index 00000000..db8857a9 Binary files /dev/null and b/repo/scenes/samples/Wikimedia Commons/image.jpg differ diff --git a/repo/scenes/samples/Wikimedia Commons/scenes.json b/repo/scenes/samples/Wikimedia Commons/scenes.json new file mode 100644 index 00000000..a3e4b611 --- /dev/null +++ b/repo/scenes/samples/Wikimedia Commons/scenes.json @@ -0,0 +1,322 @@ +[ + { + "id": "89bb1087-7cb1-4a58-92de-27fb70591907", + "name": "Wikimedia Commons", + "settings": { + "refreshInterval": 3600, + "backgroundColor": "#000000", + "execution": "interpreted" + }, + "fields": [ + { + "name": "mode", + "label": "Mode", + "type": "select", + "persist": "disk", + "access": "public", + "options": [ + "pictureOfTheDay", + "onThisDay", + "randomPictureOfTheDay", + "randomImage" + ], + "value": "pictureOfTheDay" + }, + { + "name": "secondsBetweenImages", + "label": "Seconds between images", + "type": "float", + "persist": "disk", + "access": "public", + "value": "3600" + }, + { + "name": "placement", + "label": "Image placement", + "type": "select", + "persist": "disk", + "access": "public", + "options": [ + "cover", + "contain", + "stretch", + "center" + ], + "value": "cover" + } + ], + "nodes": [ + { + "id": "82ec99e1-20e1-475e-9560-01f47bcf9d85", + "type": "event", + "position": { + "x": 78.74986072423394, + "y": 182.50696378830082 + }, + "data": { + "keyword": "render" + }, + "width": 237, + "height": 134, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 78.74986072423394, + "y": 182.50696378830082 + } + }, + { + "id": "aef22d4c-7257-4d35-87c8-a0367dcbce8a", + "type": "app", + "position": { + "x": 406.24735376044566, + "y": -61.25125348189414 + }, + "data": { + "keyword": "data/wikicommons", + "config": { + "saveAssets": "auto", + "metadataStateKey": "wikimediaMetadata" + }, + "cache": { + "enabled": false, + "inputEnabled": true, + "durationEnabled": true, + "duration": "3600" + } + }, + "width": 380, + "height": 168, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 406.24735376044566, + "y": -61.25125348189414 + } + }, + { + "id": "04a2abe4-aa72-47c3-8868-543ecab1cd7c", + "type": "state", + "position": { + "x": 366.7703514489353, + "y": -157.43568126624126 + }, + "data": { + "keyword": "mode" + }, + "width": 182, + "height": 48, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 366.7703514489353, + "y": -157.43568126624126 + } + }, + { + "id": "dbf7d3bd-9c7f-45bc-adb7-dd5a2bcaf768", + "type": "app", + "position": { + "x": 439.99832869080785, + "y": 183.75431754874649 + }, + "data": { + "keyword": "render/image", + "config": {} + }, + "width": 319, + "height": 134, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 439.99832869080785, + "y": 183.75431754874649 + } + }, + { + "id": "ab3f83e7-8b9f-4742-adbd-7dbd7929a787", + "type": "state", + "position": { + "x": 333.54293131012685, + "y": -235.7210401237126 + }, + "data": { + "keyword": "placement" + }, + "width": 284, + "height": 48, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 333.54293131012685, + "y": -235.7210401237126 + } + }, + { + "id": "11479647-0bc3-4bc3-9e8e-84ead1d378e4", + "type": "app", + "position": { + "x": 955.008913649025, + "y": 210.00668523676882 + }, + "data": { + "keyword": "logic/nextSleepDuration", + "config": {} + }, + "width": 289, + "height": 78, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 955.008913649025, + "y": 210.00668523676882 + } + }, + { + "id": "fc90ee5d-889b-41eb-a914-e74f93f21b72", + "type": "state", + "position": { + "x": 918.7576601671309, + "y": 100.00334261838438 + }, + "data": { + "keyword": "secondsBetweenImages" + }, + "width": 352, + "height": 48, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 918.7576601671309, + "y": 100.00334261838438 + } + }, + { + "id": "f91321b6-674f-4a38-949b-c85827f8dc54", + "type": "event", + "position": { + "x": 73.15692763511564, + "y": -349.62991939854413 + }, + "data": { + "keyword": "button" + }, + "width": 209, + "height": 98, + "selected": false, + "dragging": false, + "positionAbsolute": { + "x": 73.15692763511564, + "y": -349.62991939854413 + } + }, + { + "id": "6f173db6-e47f-451e-8207-ec90b3fd5294", + "position": { + "x": 330.9343459949327, + "y": -368.0584881176743 + }, + "data": { + "keyword": "logic/breakIfRendering", + "config": {} + }, + "type": "app", + "width": 271, + "height": 40, + "selected": false, + "positionAbsolute": { + "x": 330.9343459949327, + "y": -368.0584881176743 + }, + "dragging": false + }, + { + "id": "5e941a2a-077a-4fab-8c4b-a3b4c6b6d84e", + "position": { + "x": 663.471393348693, + "y": -344.43726486095784 + }, + "data": { + "keyword": "render", + "config": {} + }, + "type": "dispatch", + "width": 260, + "height": 40, + "selected": false, + "positionAbsolute": { + "x": 663.471393348693, + "y": -344.43726486095784 + }, + "dragging": false + } + ], + "edges": [ + { + "id": "94c832b8-b4bd-4907-9036-a7ce4f9c142f", + "source": "82ec99e1-20e1-475e-9560-01f47bcf9d85", + "sourceHandle": "next", + "target": "dbf7d3bd-9c7f-45bc-adb7-dd5a2bcaf768", + "targetHandle": "prev", + "type": "appNodeEdge" + }, + { + "id": "6e0ed936-3c3d-42cf-bf08-045c02db5f43", + "source": "aef22d4c-7257-4d35-87c8-a0367dcbce8a", + "sourceHandle": "fieldOutput", + "target": "dbf7d3bd-9c7f-45bc-adb7-dd5a2bcaf768", + "targetHandle": "fieldInput/image", + "type": "codeNodeEdge" + }, + { + "id": "9524c0af-b83a-420b-9b4d-d3c3eed7ce10", + "source": "04a2abe4-aa72-47c3-8868-543ecab1cd7c", + "sourceHandle": "fieldOutput", + "target": "aef22d4c-7257-4d35-87c8-a0367dcbce8a", + "targetHandle": "fieldInput/mode", + "type": "codeNodeEdge" + }, + { + "id": "d793bd19-73a8-4a85-9bb8-7b613572dce4", + "source": "ab3f83e7-8b9f-4742-adbd-7dbd7929a787", + "sourceHandle": "fieldOutput", + "target": "dbf7d3bd-9c7f-45bc-adb7-dd5a2bcaf768", + "targetHandle": "fieldInput/placement", + "type": "codeNodeEdge" + }, + { + "id": "316e9d6f-b43f-4788-bb90-0efdfd9942de", + "source": "dbf7d3bd-9c7f-45bc-adb7-dd5a2bcaf768", + "sourceHandle": "next", + "target": "11479647-0bc3-4bc3-9e8e-84ead1d378e4", + "targetHandle": "prev", + "type": "appNodeEdge" + }, + { + "id": "45ec9886-b04d-466c-ad5e-daa3188816ec", + "source": "fc90ee5d-889b-41eb-a914-e74f93f21b72", + "sourceHandle": "fieldOutput", + "target": "11479647-0bc3-4bc3-9e8e-84ead1d378e4", + "targetHandle": "fieldInput/duration", + "type": "codeNodeEdge" + }, + { + "id": "b76605bb-cc2f-4beb-b9b2-3a01b906dec3", + "target": "6f173db6-e47f-451e-8207-ec90b3fd5294", + "targetHandle": "prev", + "source": "f91321b6-674f-4a38-949b-c85827f8dc54", + "sourceHandle": "next", + "type": "appNodeEdge" + }, + { + "id": "36d8231a-46c1-4336-9d69-d17ec56fecd8", + "target": "5e941a2a-077a-4fab-8c4b-a3b4c6b6d84e", + "targetHandle": "prev", + "source": "6f173db6-e47f-451e-8207-ec90b3fd5294", + "sourceHandle": "next", + "type": "appNodeEdge" + } + ], + "apps": {} + } +] diff --git a/repo/scenes/samples/Wikimedia Commons/template.json b/repo/scenes/samples/Wikimedia Commons/template.json new file mode 100644 index 00000000..d33b18c5 --- /dev/null +++ b/repo/scenes/samples/Wikimedia Commons/template.json @@ -0,0 +1,9 @@ +{ + "name": "Wikimedia Commons", + "description": "Display Wikimedia Commons Picture of the Day or random Commons images.", + "config": null, + "image": "./image.jpg", + "imageWidth": 960, + "imageHeight": 431, + "scenes": "./scenes.json" +}