diff --git a/LaunchSchedule.js b/LaunchSchedule.js index 890fadc..6ba8238 100644 --- a/LaunchSchedule.js +++ b/LaunchSchedule.js @@ -1,163 +1,326 @@ -// LICENSE -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * LICENSE * + * This Source Code Form is subject to the terms of the * + * Mozilla Public License, v. 2.0. * + * If a copy of the MPL was not distributed with this file, * + * You can obtain one at http://mozilla.org/MPL/2.0/. * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -// A script for displaying upcoming rocket launches. -// Made by Rik Rossel for the Scriptable app on iOS and iPadOS. +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * A script for displaying upcoming rocket launches. * + * Made for the Scriptable app on iOS and iPadOS. * + * Author: Rik Rosseel * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -// CUSTOMIZABILITY (Change these variables to your liking). -// Use Little Endian (EU) date format or Middle Endian (USA) date format. -const isMEDateFormat = false; - -// Limit how many launches will be queried from the thespacedevs API. -// There is a possibility that 10 is not enough to show launches with the ID's given below in the widget. -const limit = 10 -// Compose API query url. -const url = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/?format=json&limit=" + limit +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * CUSTOMIZABILITY * + * (Change these variables to your liking). * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -// FONTS. +/* FONTS */ const primaryFont = new Font("SF Pro", 20) const secondaryFont = new Font("SF Pro", 15) const statusFont = Font.blackMonospacedSystemFont(15); -// COLORS. -// Font Colors. + +/* COLORS */ +// Text Colors. const primaryTextColor = Color.dynamic(new Color("#000000"), new Color("#ffffff")); const secondaryTextColor = Color.dynamic(new Color("#666666"), new Color("#999999")); +// Background Color. +const BGColor = Color.dynamic(new Color("#ffffff"), new Color("#1e1e1e")); // Status Colors. -// Status | Abbr. | ID +// (Status | Abbr. | ID) // Go for launch | Go | 1 const goColor = Color.dynamic(new Color("#4CBB17"), new Color("#22ba48")); // To Be Determined | TBD | 2 const TBDColor = Color.dynamic(new Color("#ff8500"), new Color("#ff8c00")); // To Be Confirmed | TBC | 8 const TBCColor = Color.dynamic(new Color("#ffba00"), new Color("#f6be00")); -// Background Color. -const BGColor = Color.dynamic(new Color("#ffffff"), new Color("#1e1e1e")); -// FORMAT STRINGS. -// Date. + +/* OTHER */ +// Use Little Endian (EU) date format or Middle Endian (USA) date format. +const isMEDateFormat = false; +// Use the script on macOS +const isOnMacOS = false; +// Limit how many launches will be queried from the thespacedevs API. +// There is a possibility that 10 is not enough to show launches with the ID's given below in the widget. +const limit = 10; + +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * END OF CUSTOMIZABILITY * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + +/** Little endian date format or Middle endian date format + * @result Little endian format iff [isMEDateFormat] is equal to false + * @result Middle endian format iff [isMEDateFormat] is equal to true + */ const dateFormat = isMEDateFormat ? "HH:mm MM/dd" : "HH:mm dd/MM"; +/** Adjust large widget launch count limit based on platform + * + * + */ +const launchCountLimit = isOnMacOS ? 5 : 6; +// WIDGET SIZES +const sizes = ["Small", "Medium", "Large"]; +// Root ListWidget +let widget = new ListWidget(); + + +// Compose API query url. +const url = "https://ll.thespacedevs.com/2.2.0/launch/upcoming/?format=json&limit=" + limit; // Get the data from the API const data = await getData(url) -// Check if script is ran in app or in widget. -if (config.runsInWidget) { - let widget = createWidget() - Script.setWidget(widget); +// Check if the script runs in the app +if (config.runsInApp) { + const message = "What is the size of the widget?"; + let size = await generateAlert(message, sizes); + switch (size) { + case 0: + buildSmallWidget(widget); + widget.presentSmall(); + break; + case 1: + buildMediumWidget(widget); + widget.presentMedium(); + break; + case 2: + buildLargeWidget(widget); + widget.presentLarge(); + break; + default: + buildInvalidParamWidget(widget); + widget.presentSmall(); + break; + } Script.complete(); -} else { // Runs in app - let widget = createLargeWidget(); - widget.presentLarge(); - if (config.runsWithSiri) { - Speech.speak("Here are the next few upcoming launches") +} +// Or in the widget +else if (config.runsInWidget) { + let sizeArg = args.widgetParameter.replace(/\s/g, ''); + sizeArg = sizeArg[0].toUpperCase() + sizeArg.substring(1); + let size = sizes.indexOf(sizeArg); + if (size == -1) { + buildInvalidParamWidget(widget); + widget.presentSmall(); + } else { + switch (size) { + case 0: + buildSmallWidget(widget); + widget.presentSmall(); + break; + case 1: + buildMediumWidget(widget); + widget.presentMedium(); + break; + case 2: + buildLargeWidget(widget); + widget.presentLarge(); + break; + default: + buildInvalidParamWidget(widget); + widget.presentSmall(); + break; + } } Script.complete(); } +/** + * Build a widget to let the user know they gave an invalid parameter. + * + * @param {ListWidget} widget The widget to add content to. + */ +function buildInvalidParamWidget(widget) { + widget.backgroundColor = BGColor; + let title = widget.addText("Invalid size parameter."); + title.font = primaryFont; + title.textColor = primaryTextColor; + let info = widget.addText( + "The valid sizes are: Small, Medium or Large" + ); + info.font = secondaryFont; + info.textColor = secondaryTextColor; +} +/** + * Build a widget to let the user know that the results couldn't be fetched. + * + * @param {ListWidget} widget The widget to add content to. + */ + function buildUnableToFetchWidget(widget) { + title = widget.addText("Unable to fetch upcoming launches.") + title.font = primaryFont + title.textColor = primaryTextColor + detail = widget.addText(data.detail) + detail.font = secondaryFont + detail.textColor = secondaryTextColor +} -/** - * Create a Widget with a first upcoming launch - * and several more upcoming launches with less details. +/** + * Build a widget (small) with information of the first upcoming launch. * - * @returns {ListWidget} The widget to show. + * @param {ListWidget} widget The widget to add content to. */ -function createWidget() { - // Root widget element. - const w = new ListWidget() - w.backgroundColor = BGColor - w.addSpacer(4) +function buildSmallWidget(widget) { + widget.backgroundColor = BGColor; + widget.addSpacer(4); + + // Gaurd clause for data. + if (!data.results) { + buildUnableToFetchWidget(widget); + return; + } + + var firstLaunchIndex = 0 + // Check for first launch that is to be determined, to be confirmed or is go for launch. + while ( + firstLaunchIndex < data.results.length + && !isValidStatus(data.results[firstLaunchIndex].status.id) + ) { + firstLaunchIndex++; + } + // Text for the first upcoming luanch. + const firstLaunchName = widget.addText(data.results[firstLaunchIndex].name); + firstLaunchName.font = primaryFont; + firstLaunchName.textColor = primaryTextColor; + + widget.addSpacer(4); + // first launch status stack. + const statusStack = widget.addStack(); + statusStack.cornerRadius = 10; + statusStack.setPadding(2, 7, 2, 7); + statusStack.backgroundColor = getStatusColor(data.results[firstLaunchIndex].status.id); + + widget.addSpacer(5); - // Guard clause for data + // First launch status text. + const firstLaunchStatusText = statusStack.addText(data.results[firstLaunchIndex].status.abbrev); + firstLaunchStatusText.textColor = BGColor; + firstLaunchStatusText.font = statusFont; + + // First launch time and date. + const launchTimeText = widget.addText(launchTimeFormatter(data.results[firstLaunchIndex].net)); + launchTimeText.textColor = secondaryTextColor; + launchTimeText.font = new Font("SF Pro", 15); + +} + +/** + * Build a widget (medium) with information of the first few upcoming launches. + * + * @param {ListWidget} widget The widget to add content to. + */ +function buildMediumWidget(widget) { + widget.backgroundColor = BGColor; + widget.addSpacer(4); + + // Guard clause for data. if (!data.results) { - return noDataWidget(w); + buildUnableToFetchWidget(widget); + return; } - let firstLaunchIndex = 0 + let firstLaunchIndex = 0; // Check for first launch that is to be determined, to be confirmed or is go for launch. - while (firstLaunchIndex < data.results.length && !isValidStatus(data.results[firstLaunchIndex].status.id)) { - firstLaunchIndex++ + while ( + firstLaunchIndex < data.results.length + && !isValidStatus(data.results[firstLaunchIndex].status.id) + ) { + firstLaunchIndex++; } // Text for the first upcoming luanch. - const firstLaunchName = w.addText(data.results[firstLaunchIndex].name) - firstLaunchName.font = primaryFont - firstLaunchName.textColor = primaryTextColor + const firstLaunchName = widget.addText(data.results[firstLaunchIndex].name); + firstLaunchName.font = primaryFont; + firstLaunchName.textColor = primaryTextColor; - w.addSpacer(4) + widget.addSpacer(4); // Stack for info of first launch (status, time and date). - const infoStack = w.addStack() - infoStack.centerAlignContent() + const infoStack = widget.addStack(); + infoStack.centerAlignContent(); // first launch status stack. - const statusStack = infoStack.addStack() - statusStack.cornerRadius = 10 - statusStack.setPadding(2, 7, 2, 7) - statusStack.backgroundColor = getStatusColor(data.results[firstLaunchIndex].status.id) + const statusStack = infoStack.addStack(); + statusStack.cornerRadius = 10; + statusStack.setPadding(2, 7, 2, 7); + statusStack.backgroundColor = getStatusColor(data.results[firstLaunchIndex].status.id); - infoStack.addSpacer(10) + infoStack.addSpacer(10); // First launch status text. - const firstLaunchStatusText = statusStack.addText(data.results[firstLaunchIndex].status.name) - firstLaunchStatusText.textColor = BGColor - firstLaunchStatusText.font = statusFont + const firstLaunchStatusText = statusStack.addText(data.results[firstLaunchIndex].status.name); + firstLaunchStatusText.textColor = BGColor; + firstLaunchStatusText.font = statusFont; // First launch time and date. const launchTimeText = infoStack.addText(launchTimeFormatter(data.results[firstLaunchIndex].net)); launchTimeText.textColor = secondaryTextColor; - launchTimeText.font = new Font("SF Pro", 15); + launchTimeText.font = secondaryFont; - w.addSpacer(10); + widget.addSpacer(4) // Remaining upcoming launches. - var count = 0 - for (var i = firstLaunchIndex + 1; i < data.results.length; i++) { + let count = 0 + for (let i = firstLaunchIndex + 1; i < data.results.length; i++) { if (count == 3) { break; } else if (!isValidStatus(data.results[i].status.id)) { - continue; - } - const upcomingStack = w.addStack(); + continue; + } + const upcomingStack = widget.addStack(); upcomingStack.centerAlignContent(); const point = upcomingStack.addText("•"); - point.font = Font.blackMonospacedSystemFont(25); + point.font = Font.blackMonospacedSystemFont(25); point.textColor = getStatusColor(data.results[i].status.id); upcomingStack.addSpacer(4); const upcomingLaunchName = upcomingStack.addText(data.results[i].name); upcomingLaunchName.textColor = primaryTextColor; + + // Increment launch count. count++; } - return w; } -/** - * Function that creates a large Widget with several upcoming launches. - * - * @returns {ListWidget} widget +/** + * Build a widget (large) with information of the first few upcoming launches. + * @param {ListWidget} widget The widget to add content to. */ - function createLargeWidget() { - let w = new ListWidget(); - w.backgroundColor = BGColor; - // Guard clause for data +function buildLargeWidget(widget) { + widget.backgroundColor = BGColor; + widget.addSpacer(4); + + // Guard clause for data. if (!data.results) { - return noDataWidget(w); + buildUnableToFetchWidget(widget); + return; } - + let count = 0; for (launch of data.results) { - if (!isValidStatus(launch.status.id) || count >= 5) { + // Check if the status ID is valid. + if (!isValidStatus(launch.status.id)) { continue; } - let launchName = w.addText(launch.name); + // Check if the limit of 6 launches is reached. + if (count >= launchCountLimit) { + break; + } + // Text for upcoming launch. + let launchName = widget.addText(launch.name); launchName.font = primaryFont; launchName.textColor = primaryTextColor; - w.addSpacer(4); + widget.addSpacer(4); - let infoStack = w.addStack(); + // Stack fo rinfo of the launch (status, time and date). + let infoStack = widget.addStack(); infoStack.layoutHorizontally(); + + // Launch status stack. let statusStack = infoStack.addStack(); let statusBGColor = getStatusColor(launch.status.id); statusStack.backgroundColor = statusBGColor; @@ -170,56 +333,33 @@ function createWidget() { infoStack.addSpacer(10); let launchTimeText = infoStack.addText(launchTimeFormatter(launch.net)); - launchTimeText.font = secondaryFont; - launchTimeText.textColor = secondaryTextColor; + launchTimeText.font = secondaryFont; + launchTimeText.textColor = secondaryTextColor; - w.addSpacer(15); + widget.addSpacer(10); count++; } - return w; } -/** - * Get data from url and load it as JSON. - * @param {string} url The API url query. - * @returns {Promise} Data from the url. - */ -async function getData(url) { - const r = new Request(url); - const data = await r.loadJSON(); - return data; -} - -/** - * Create a widget to show when no data was available. - * @param {ListWidget} widget The widget to build on. - * @returns {ListWidget} The widget to show. - */ -function noDataWidget(widget) { - title = widget.addText("Unable to fetch upcoming launches."); - title.font = primaryFont; - title.textColor = primaryTextColor; - detail = widget.addText(data.detail); - detail.font = secondaryFont; - detail.textColor = secondaryTextColor; - return widget; -} +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * HELPER FUNCTIONS * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /** - * Check if the given status ID is a valid status ID - * @param {int} statusID The integer id for the status. - * @returns {bool} True iff (statusID == 1 || statusID == 2 || statusID == 8). + * A function to determine the validity of a status ID. + * @param {int} statusID The status ID to check. + * @returns {bool} Returns true iff statusID == 1 || statusID == 2 || statusID == 8 */ function isValidStatus(statusID) { return statusID == 1 || statusID == 2 || statusID == 8; } /** - * Get the color for the given status ID. - * @param {int} statusID The integer id for the status. - * @returns {Color} The color for the status iff isValidStatus(statusID) == true, else null. + * A function to get the appropriate color for a given status ID. + * @param {int} statusID The status ID to get the color for. + * @returns {Color} Returns goColor iff statusID == 1 || TBDColor iff statusID == 2 || TBCColor iff statusID == 8. */ function getStatusColor(statusID) { let statusColor; @@ -238,9 +378,24 @@ function getStatusColor(statusID) { } /** - * Format the given date to Little Endian or Middle Endian format. - * @param {string} date The date in ISO format - * @returns {string} The date in Little Endian or Middle Endian format. + * Get data from url and load it as JSON. + * @param {String} url The API url query. + * @returns {Promise} Data from the url. + */ +async function getData(url) { + const r = new Request(url); + const data = await r.loadJSON(); + return data; +} + +/** + * Format date into LE or ME date format. + * + * To change the date format from LE to ME change the boolean isMEDateFormat to true + * at the top of the file in the CUSTOMIZABILITY section. + * + * @param {String} Date The date to format (in ISO format). + * @returns {String} The launch time formatted according to the specified date format. */ function launchTimeFormatter(date) { let launchTime = new Date(date); @@ -249,3 +404,20 @@ function launchTimeFormatter(date) { return df.string(launchTime); } +/** + * Generate, present and return the selected option index to the user. + * + * @param {String} message The message to present in the alert. + * @param {String[]} options The options to present in the alert. + * @returns {int} The option index selected by the user. + */ +async function generateAlert(message, options) { + let alert = new Alert(); + alert.message = message; + for (const option of options) { + alert.addAction(option); + } + + let responseIndex = await alert.presentAlert(); + return responseIndex; +} diff --git a/README.md b/README.md index 3f40449..90ee5b3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,40 @@ # launch-schedule A script for displaying upcoming rocket launches in the Scriptable widget app for iOS. - - +## Configuration guide +There are a few options at the beginning of the script that can be changed to your liking. +* Colors +* Fonts +* Date format (Little endian (dd/mm/yyyy) or Middle endian (mm/dd/yyy) +* Running on macOS + +## Running in Widget +**IMPORTANT!** When running the script in a widget be sure to edit the widget (on iOS press and hold on the widget, then choose 'Edit "Scriptable"') and fill in the size of the widget in the parameter field. +There are 3 options: +* Small +* Medium +* Large + +(e.g.: when using a medium sized widget fill in `Medium`)\ +If the parameter is missing or misspelled the widget will display a placeholder widget to fill in or edit the parameter field. + +## Light mode previews + + + + +## Dark mode previews + + + + + + +\ You can find the Scriptable app at https://scriptable.app/ or visit the [Apple App Store](https://apps.apple.com/us/app/scriptable/id1405459188). View [source code](https://github.com/rik-rosseel/launch-schedule) on Github. For business inquiries contact Rik Rosseel at contact@rikrosseel.com. +