Last active
July 31, 2021 04:52
-
-
Save drzoidbergh/73be57f39ce2de4193de38bab6a5d7ae to your computer and use it in GitHub Desktop.
Scriptable iOS widget to display the next spacecraft launches. Uses Space Devs Launch Library API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Variables used by Scriptable. | |
| // These must be at the very top of the file. Do not edit. | |
| // icon-color: darkgray; icon-glyph: rocket; | |
| // | |
| // Copyright (C) 2020 | |
| // | |
| // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, | |
| // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER | |
| // IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE | |
| // OF THIS SOFTWARE. | |
| // | |
| // Data courtesy of The Space Devs https://thespacedevs.com/ | |
| // Launch Library API https://ll.thespacedevs.com/2.0.0/swagger | |
| // Using the upcoming Launch List Endpoint https://ll.thespacedevs.com/2.0.0/launch/upcoming/ | |
| // | |
| // By default this widget displays the next launch by SpaceX. | |
| // This widget may be parametrized when placed on the homescreen: supply either the company name | |
| // you want to display or "all" to see all companies | |
| const COMPANY_PARAM = '&rocket__configuration__manufacturer__name=' | |
| const DEFAULT_COMPANY = 'SpaceX' | |
| const API_URL_LAUNCH = 'https://ll.thespacedevs.com/2.0.0/launch/upcoming/?limit=1'; | |
| let company | |
| if (args.widgetParameter) { | |
| if (args.widgetParameter !== "all"){ | |
| company = args.widgetParameter; | |
| } | |
| } else { | |
| company = DEFAULT_COMPANY | |
| } | |
| const widget = await createWidget() | |
| if (!config.runsInWidget) { | |
| await widget.presentMedium() | |
| } | |
| Script.setWidget(widget) | |
| Script.complete() | |
| async function createWidget() { | |
| const launchData = await loadData() | |
| if (launchData.error){ | |
| console.error(launchData.error); | |
| } | |
| const name = launchData.rocketName; | |
| const bgImage = await loadImage(launchData.imageUrl); | |
| const list = new ListWidget() | |
| list.backgroundImage = bgImage; | |
| list.setPadding(10,10,10,10); | |
| const textColor = Color.white() | |
| const textBgColor = new Color('#101010',0.4) | |
| const outer = list.addStack() | |
| outer.layoutVertically() | |
| outer.backgroundColor = textBgColor | |
| outer.backgroundColor = new Color('#101010',0.4) | |
| outer.useDefaultPadding() | |
| outer.cornerRadius = 10 | |
| textBlock(outer.addStack(), (stack)=>{ | |
| stack.setPadding(5, 0, 0, 0) | |
| const launchDate = stack.addDate(launchData.planned) | |
| launchDate.rightAlignText() | |
| launchDate.applyDateStyle() | |
| launchDate.font = Font.mediumSystemFont(13) | |
| launchDate.textColor = textColor | |
| stack.addSpacer(4) | |
| const launchTime = stack.addDate(launchData.planned) | |
| launchTime.rightAlignText() | |
| launchTime.applyTimeStyle() | |
| launchTime.font = Font.mediumSystemFont(13) | |
| launchTime.textColor = textColor | |
| }) | |
| outer.addSpacer() | |
| textBlock(outer.addStack(), (stack)=>{ | |
| const header = stack.addText(name); | |
| header.font = Font.headline() | |
| header.textColor = textColor | |
| }) | |
| textBlock(outer.addStack(), (stack) => { | |
| const mission = stack.addText(launchData.mission) | |
| mission.font = Font.mediumSystemFont(13) | |
| mission.textColor = textColor | |
| const missionType = stack.addText(launchData.missionType); | |
| missionType.font = Font.mediumSystemFont(13); | |
| missionType.textColor = textColor | |
| }) | |
| outer.addSpacer() | |
| textBlock(outer.addStack(), (stack) => { | |
| const launchLocation = stack.addText(launchData.launchLocation); | |
| launchLocation.centerAlignText() | |
| launchLocation.font = Font.mediumSystemFont(13) | |
| launchLocation.textColor = textColor | |
| }) | |
| outer.addSpacer() | |
| textBlock(outer.addStack(), (stack) => { | |
| const launchCountdownStack = stack.addStack() | |
| launchCountdownStack.backgroundColor = new Color('#101010',0.4) | |
| launchCountdownStack.setPadding(5, 20, 5, 20) | |
| launchCountdownStack.cornerRadius = 4 | |
| const launchCountdown = launchCountdownStack.addDate(launchData.planned) | |
| launchCountdown.centerAlignText() | |
| launchCountdown.applyTimerStyle() | |
| launchCountdown.font = Font.callout(15) | |
| launchCountdown.textColor = textColor | |
| }) | |
| return list; | |
| } | |
| function textBlock(stack,contentMaker){ | |
| stack.addSpacer() | |
| stack.centerAlignContent() | |
| contentMaker(stack) | |
| stack.addSpacer() | |
| } | |
| // access to the api is throttled so we try to minimize reads to once a day or after launch | |
| async function loadData() { | |
| const cached = readFromCache(company); | |
| if (cached && isNotOutdated(cached)) { | |
| console.log(cached) | |
| return cached; | |
| } | |
| let newData = await loadNewData(); | |
| if (newData.error) { | |
| // ran into an error retrieving new data: use outdated cache | |
| // if (cached){ | |
| // return cached; | |
| // } | |
| return newData | |
| } | |
| storeToCache(company, newData); | |
| return newData; | |
| } | |
| function isNotOutdated(data){ | |
| const now = new Date() | |
| const yesterday = new Date() | |
| yesterday.setDate(now.getDate() -1) | |
| return now < data.planned && yesterday < data.retrieved | |
| } | |
| async function loadNewData() { | |
| let uri = API_URL_LAUNCH + (company ? COMPANY_PARAM + company : '') | |
| console.log(uri); | |
| let req = new Request(uri); | |
| let rawData = await req.loadJSON(); | |
| const statusCode = req.response.statusCode; | |
| console.log(`response statusCode ${statusCode}`) | |
| if (statusCode === 200 && rawData){ | |
| const launchData = rawData.results[0]; | |
| const extractedData = { | |
| "planned": new Date(launchData.net), | |
| "rocketName":launchData.rocket.configuration.name, | |
| "mission": launchData.mission.name, | |
| "missionType": launchData.mission.type, | |
| "launchLocation": launchData.pad.location.name, | |
| "imageUrl": launchData.image, | |
| "retrieved": new Date(), | |
| } | |
| return extractedData; | |
| } else if (statusCode === 429) { | |
| console.log(rawData) | |
| return {"error": "" +rawData.detail}; | |
| } | |
| return {"error": "could not load data"} | |
| } | |
| // helper function to download an image from a given url | |
| async function loadImage(imgUrl) { | |
| const req = new Request(imgUrl) | |
| return await req.loadImage() | |
| } | |
| function readFromCache(key) { | |
| const fm = FileManager.local() | |
| const path = fm.joinPath(fm.documentsDirectory(), 'nextlaunches_' + key + '.json') | |
| if (fm.fileExists(path)) { | |
| let data = fm.readString(path) | |
| console.log(`Local: read ${path}`) | |
| return JSON.parse(data, parseIsoDateStrToDate) | |
| } | |
| } | |
| function parseIsoDateStrToDate(key, value){ | |
| const isoDateFormat = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?Z$/; | |
| if (typeof value === "string" && isoDateFormat.test(value)){ | |
| return new Date(value); | |
| } | |
| return value | |
| } | |
| function storeToCache(key, data) { | |
| const fm = FileManager.local() | |
| const path = fm.joinPath(fm.documentsDirectory(), 'nextlaunches_' + key + '.json') | |
| fm.writeString(path, JSON.stringify(data)) | |
| console.log(`save ${path} to local`) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment