Skip to content

Instantly share code, notes, and snippets.

@drzoidbergh
Last active July 31, 2021 04:52
Show Gist options
  • Select an option

  • Save drzoidbergh/73be57f39ce2de4193de38bab6a5d7ae to your computer and use it in GitHub Desktop.

Select an option

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
// 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