From 1d04e3e1d7972730acaf8f59a38a799ed40d4f27 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Sat, 24 Oct 2020 11:54:00 +0300 Subject: [PATCH 01/21] Fix The weather data parser --- scripts/TermiWidget.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/TermiWidget.js b/scripts/TermiWidget.js index 204cb5e..b3fb5ab 100644 --- a/scripts/TermiWidget.js +++ b/scripts/TermiWidget.js @@ -149,12 +149,12 @@ async function fetchWeather() { return { location: address[0].locality, - icon: getWeatherEmoji(data.current.weather[0].id, ((new Date()).getTime() / 1000) >= data.current.sunset), - description: data.current.weather[0].main, - temperature: Math.round(data.current.temp), - wind: Math.round(data.current.wind_speed), - high: Math.round(data.daily[0].temp.max), - low: Math.round(data.daily[0].temp.min), + icon: getWeatherEmoji(data.weather[0].id, ((new Date()).getTime() / 1000) >= data.sys.sunset), + description: data.weather[0].main, + temperature: Math.round(data.main.temp), + wind: Math.round(data.wind.speed), + high: Math.round(data.main.temp_max), + low: Math.round(data.main.temp_min), } } From 0e741e4f4f50d497e5e4de15743c37860343c66c Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 07:57:01 +0300 Subject: [PATCH 02/21] westher-cal --- westher-cal.js | 1408 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1408 insertions(+) create mode 100644 westher-cal.js diff --git a/westher-cal.js b/westher-cal.js new file mode 100644 index 0000000..77224db --- /dev/null +++ b/westher-cal.js @@ -0,0 +1,1408 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: teal; icon-glyph: magic; +/* + * SETUP + * Use this section to set up the widget. + * ====================================== + */ + +// To use weather, get a free API key at openweathermap.org/appid and paste it in between the quotation marks. +const apiKey = "" + +// Set the locale code. Leave blank "" to match the device's locale. You can change the hard-coded text strings in the TEXT section below. +let locale = "en" + +// Set to true for fixed location, false to update location as you move around +const lockLocation = true + +// The size of the widget preview in the app. +const widgetPreview = "large" + +// Set to true for an image background, false for no image. +const imageBackground = true + +// Set to true to reset the widget's background image. +const forceImageUpdate = false + +// Set the padding around each item. Default is 5. +const padding = 5 + +/* + * LAYOUT + * Decide what items to show on the widget. + * ======================================== + */ + +// You always need to start with "row," and "column," items, but you can now add as many as you want. +// Adding left, right, or center will align everything after that. The default alignment is left. + +// You can add a flexible vertical space with "space," or a fixed-size space like this: "space(50)" +// Align items to the top or bottom of columns by adding "space," before or after all items in the column. + +// There are many possible items, including: date, greeting, events, current, future, battery, sunrise, and text("Your text here") +// Make sure to always put a comma after each item. + +const items = [ + + row, + + column, + date, + battery, + sunrise, + space, + + column(90), + current, + future, + + row, + + column, + events, + +] + +/* + * ITEM SETTINGS + * Choose how each item is displayed. + * ================================== + */ + +// DATE +// ==== +const dateSettings = { + + // If set to true, date will become smaller when events are displayed. + dynamicDateSize: false + + // If the date is not dynamic, should it be large or small? + ,staticDateSize: "large" + + // Determine the date format for each date type. See docs.scriptable.app/dateformatter + ,smallDateFormat: "EEEE, MMMM d" + ,largeDateLineOne: "EEEE," + ,largeDateLineTwo: "MMMM d" +} + +// EVENTS +// ====== +const eventSettings = { + + // How many events to show. + numberOfEvents: 3 + + // Show all-day events. + ,showAllDay: true + + // Show tomorrow's events. + ,showTomorrow: true + + // Can be blank "" or set to "duration" or "time" to display how long an event is. + ,showEventLength: "duration" + + // Set which calendars for which to show events. Empty [] means all calendars. + ,selectCalendars: [] + + // Leave blank "" for no color, or specify shape (circle, rectangle) and/or side (left, right). + ,showCalendarColor: "rectangle left" + + // When no events remain, show a hard-coded "message", a "greeting", or "none". + ,noEventBehavior: "message" +} + +// SUNRISE +// ======= +const sunriseSettings = { + + // How many minutes before/after sunrise or sunset to show this element. 0 for always. + showWithin: 0 +} + +// WEATHER +// ======= +const weatherSettings = { + + // Set to imperial for Fahrenheit, or metric for Celsius + units: "metric" + + // Show the location of the current weather. + ,showLocation: false + + // Show the text description of the current conditions. + ,showCondition: false + + // Show today's high and low temperatures. + ,showHighLow: true + + // Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it. + ,tomorrowShownAtHour: 20 +} + +/* + * TEXT + * Change the language and formatting of text displayed. + * ===================================================== + */ + +// You can change the language or wording of any text in the widget. +const localizedText = { + + // The text shown if you add a greeting item to the layout. + nightGreeting: "Good night." + ,morningGreeting: "Good morning." + ,afternoonGreeting: "Good afternoon." + ,eveningGreeting: "Good evening." + + // The text shown if you add a future weather item to the layout, or tomorrow's events. + ,nextHourLabel: "Next hour" + ,tomorrowLabel: "Tomorrow" + + // Shown when noEventBehavior is set to "message". + ,noEventMessage: "Enjoy the rest of your day." + + // The text shown after the hours and minutes of an event duration. + ,durationMinute: "m" + ,durationHour: "h" + +} + +// Set the font, size, and color of various text elements. Use iosfonts.com to find fonts to use. If you want to use the default iOS font, set the font name to one of the following: ultralight, light, regular, medium, semibold, bold, heavy, black, or italic. +const textFormat = { + + // Set the default font and color. + defaultText: { size: 14, color: "ffffff", font: "regular" }, + + // Any blank values will use the default. + smallDate: { size: 17, color: "", font: "semibold" }, + largeDate1: { size: 30, color: "", font: "light" }, + largeDate2: { size: 30, color: "", font: "light" }, + + greeting: { size: 30, color: "", font: "semibold" }, + eventLabel: { size: 14, color: "", font: "semibold" }, + eventTitle: { size: 14, color: "", font: "semibold" }, + eventTime: { size: 14, color: "ffffffcc", font: "" }, + noEvents: { size: 30, color: "", font: "semibold" }, + + largeTemp: { size: 34, color: "", font: "light" }, + smallTemp: { size: 14, color: "", font: "" }, + tinyTemp: { size: 12, color: "", font: "" }, + + customText: { size: 14, color: "", font: "" }, + + battery: { size: 14, color: "", font: "medium" }, + sunrise: { size: 14, color: "", font: "medium" }, +} + +/* + * WIDGET CODE + * Be more careful editing this section. + * ===================================== + */ + +// Make sure we have a locale value. +if (locale == "" || locale == null) { locale = Device.locale() } + +// Declare the data variables. +var eventData, locationData, sunData, weatherData + +// Create global constants. +const currentDate = new Date() +const files = FileManager.local() + +/* + * CONSTRUCTION + * ============ + */ + +// Set up the widget with padding. +const widget = new ListWidget() +const horizontalPad = padding < 10 ? 10 - padding : 10 +const verticalPad = padding < 15 ? 15 - padding : 15 +widget.setPadding(horizontalPad, verticalPad, horizontalPad, verticalPad) +widget.spacing = 0 + +// Set up the global variables. +var currentRow = {} +var currentColumn = {} + +// Set up the initial alignment. +var currentAlignment = alignLeft + +// Set up the global ASCII variables. +var currentColumns = [] +var rowNeedsSetup = false + +// It's ASCII time! +if (typeof items[0] == 'string') { + for (line of items[0].split(/\r?\n/)) { await processLine(line) } +} +// Otherwise, set up normally. +else { + for (item of items) { await item(currentColumn) } +} + +/* + * BACKGROUND DISPLAY + * ================== + */ + +// If it's an image background, display it. +if (imageBackground) { + + // Determine if our image exists and when it was saved. + const path = files.joinPath(files.documentsDirectory(), "weather-cal-image") + const exists = files.fileExists(path) + + // If it exists and an update isn't forced, use the cache. + if (exists && (config.runsInWidget || !forceImageUpdate)) { + widget.backgroundImage = files.readImage(path) + + // If it's missing when running in the widget, use a gray background. + } else if (!exists && config.runsInWidget) { + widget.backgroundColor = Color.gray() + + // But if we're running in app, prompt the user for the image. + } else { + const img = await Photos.fromLibrary() + widget.backgroundImage = img + files.writeImage(path, img) + } + +// If it's not an image background, show the gradient. +} else { + let gradient = new LinearGradient() + let gradientSettings = await setupGradient() + + gradient.colors = gradientSettings.color() + gradient.locations = gradientSettings.position() + + widget.backgroundGradient = gradient +} + +// Finish the widget and show a preview. +Script.setWidget(widget) +if (widgetPreview == "small") { widget.presentSmall() } +else if (widgetPreview == "medium") { widget.presentMedium() } +else if (widgetPreview == "large") { widget.presentLarge() } +Script.complete() + +/* + * ASCII FUNCTIONS + * Now isn't this a lot of fun? + * ============================ + */ + +// Provide the named function. +function provideFunction(name) { + const functions = { + space() { return space }, + left() { return left }, + right() { return right }, + center() { return center }, + date() { return date }, + greeting() { return greeting }, + events() { return events }, + current() { return current }, + future() { return future }, + battery() { return battery }, + sunrise() { return sunrise }, + } + return functions[name] +} + +// Processes a single line of ASCII. +async function processLine(lineInput) { + + // Because iOS loves adding periods to everything. + const line = lineInput.replace(/\.+/g,'') + + // If it's blank, return. + if (line.trim() == '') { return } + + // If it's a line, enumerate previous columns (if any) and set up the new row. + if (line[0] == '-' && line[line.length-1] == '-') { + if (currentColumns.length > 0) { await enumerateColumns() } + rowNeedsSetup = true + return + } + + // If it's the first content row, finish the row setup. + if (rowNeedsSetup) { + row(currentColumn) + rowNeedsSetup = false + } + + // If there's a number, this is a setup row. + const setupRow = line.match(/\d+/) + + // Otherwise, it has columns. + const items = line.split('|') + + // Iterate through each item. + for (var i=1; i < items.length-1; i++) { + + // If the current column doesn't exist, make it. + if (!currentColumns[i]) { currentColumns[i] = { items: [] } } + + // Now we have a column to add the items to. + const column = currentColumns[i].items + + // Get the current item and its trimmed version. + const item = items[i] + const trim = item.trim() + + // If it's not a function, figure out spacing. + if (!provideFunction(trim)) { + + // If it's a setup row, whether or not we find the number, we keep going. + if (setupRow) { + const value = parseInt(trim, 10) + if (value) { currentColumns[i].width = value } + continue + } + + // If it's blank and we haven't already added a space, add one. + const prevItem = column[column.length-1] + if (trim == '' && (!prevItem || (prevItem && !prevItem.startsWith("space")))) { + column.push("space") + } + + // Either way, we're done. + continue + + } + + // Determine the alignment. + const index = item.indexOf(trim) + const length = item.slice(index,item.length).length + + let align + if (index > 0 && length > trim.length) { align = "center" } + else if (index > 0) { align = "right" } + else { align = "left" } + + // Add the items to the column. + column.push(align) + column.push(trim) + } +} + +// Runs the function names in each column. +async function enumerateColumns() { + if (currentColumns.length > 0) { + for (col of currentColumns) { + + // If it's null, go to the next one. + if (!col) { continue } + + // If there's a width, use the width function. + if (col.width) { + column(col.width)(currentColumn) + + // Otherwise, create the column normally. + } else { + column(currentColumn) + } + for (item of col.items) { + const func = provideFunction(item)() + await func(currentColumn) + } + } + currentColumns = [] + } +} + +/* + * LAYOUT FUNCTIONS + * These functions manage spacing and alignment. + * ============================================= + */ + +// Makes a new row on the widget. +function row(input = null) { + + function makeRow() { + currentRow = widget.addStack() + currentRow.layoutHorizontally() + currentRow.setPadding(0, 0, 0, 0) + currentColumn.spacing = 0 + + // If input was given, make a column of that size. + if (input > 0) { currentRow.size = new Size(0,input) } + } + + // If there's no input or it's a number, it's being called in the layout declaration. + if (!input || typeof input == "number") { return makeRow } + + // Otherwise, it's being called in the generator. + else { makeRow() } +} + +// Makes a new column on the widget. +function column(input = null) { + + function makeColumn() { + currentColumn = currentRow.addStack() + currentColumn.layoutVertically() + currentColumn.setPadding(0, 0, 0, 0) + currentColumn.spacing = 0 + + // If input was given, make a column of that size. + if (input > 0) { currentColumn.size = new Size(input,0) } + } + + // If there's no input or it's a number, it's being called in the layout declaration. + if (!input || typeof input == "number") { return makeColumn } + + // Otherwise, it's being called in the generator. + else { makeColumn() } +} + +// Create an aligned stack to add content to. +function align(column) { + + // Add the containing stack to the column. + let alignmentStack = column.addStack() + alignmentStack.layoutHorizontally() + + // Get the correct stack from the alignment function. + let returnStack = currentAlignment(alignmentStack) + returnStack.layoutVertically() + return returnStack +} + +// Create a right-aligned stack. +function alignRight(alignmentStack) { + alignmentStack.addSpacer() + let returnStack = alignmentStack.addStack() + return returnStack +} + +// Create a left-aligned stack. +function alignLeft(alignmentStack) { + let returnStack = alignmentStack.addStack() + alignmentStack.addSpacer() + return returnStack +} + +// Create a center-aligned stack. +function alignCenter(alignmentStack) { + alignmentStack.addSpacer() + let returnStack = alignmentStack.addStack() + alignmentStack.addSpacer() + return returnStack +} + +// This function adds a space, with an optional amount. +function space(input = null) { + + // This function adds a spacer with the input width. + function spacer(column) { + + // If the input is null or zero, add a flexible spacer. + if (!input || input == 0) { column.addSpacer() } + + // Otherwise, add a space with the specified length. + else { column.addSpacer(input) } + } + + // If there's no input or it's a number, it's being called in the column declaration. + if (!input || typeof input == "number") { return spacer } + + // Otherwise, it's being called in the column generator. + else { input.addSpacer() } +} + +// Change the current alignment to right. +function right(x) { currentAlignment = alignRight } + +// Change the current alignment to left. +function left(x) { currentAlignment = alignLeft } + +// Change the current alignment to center. +function center(x) { currentAlignment = alignCenter } + +/* + * SETUP FUNCTIONS + * These functions prepare data needed for items. + * ============================================== + */ + +// Set up the eventData object. +async function setupEvents() { + + eventData = {} + const calendars = eventSettings.selectCalendars + const numberOfEvents = eventSettings.numberOfEvents + + // Function to determine if an event should be shown. + function shouldShowEvent(event) { + + // If events are filtered and the calendar isn't in the selected calendars, return false. + if (calendars.length && !calendars.includes(event.calendar.title)) { return false } + + // Hack to remove canceled Office 365 events. + if (event.title.startsWith("Canceled:")) { return false } + + // If it's an all-day event, only show if the setting is active. + if (event.isAllDay) { return eventSettings.showAllDay } + + // Otherwise, return the event if it's in the future. + return (event.startDate.getTime() > currentDate.getTime()) + } + + // Determine which events to show, and how many. + const todayEvents = await CalendarEvent.today([]) + let shownEvents = 0 + let futureEvents = [] + + for (const event of todayEvents) { + if (shownEvents == numberOfEvents) { break } + if (shouldShowEvent(event)) { + futureEvents.push(event) + shownEvents++ + } + } + + // If there's room and we need to, show tomorrow's events. + let multipleTomorrowEvents = false + if (eventSettings.showTomorrow && shownEvents < numberOfEvents) { + + const tomorrowEvents = await CalendarEvent.tomorrow([]) + for (const event of tomorrowEvents) { + if (shownEvents == numberOfEvents) { break } + if (shouldShowEvent(event)) { + + // Add the tomorrow label prior to the first tomorrow event. + if (!multipleTomorrowEvents) { + + // The tomorrow label is pretending to be an event. + futureEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isLabel: true }) + multipleTomorrowEvents = true + } + + // Show the tomorrow event and increment the counter. + futureEvents.push(event) + shownEvents++ + } + } + } + + // Store the future events, and whether or not any events are displayed. + eventData.futureEvents = futureEvents + eventData.eventsAreVisible = (futureEvents.length > 0) && (eventSettings.numberOfEvents > 0) +} + +// Set up the gradient for the widget background. +async function setupGradient() { + + // Requirements: sunrise + if (!sunData) { await setupSunrise() } + + let gradient = { + dawn: { + color() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] }, + position() { return [0, 0.5, 1] }, + }, + + sunrise: { + color() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] }, + position() { return [0, 0.8, 1.5] }, + }, + + midday: { + color() { return [new Color("3a8cc1"), new Color("90c0df")] }, + position() { return [0, 1] }, + }, + + noon: { + color() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] }, + position() { return [-0.2, 0.2, 1.5] }, + }, + + sunset: { + color() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] }, + position() { return [0.1, 0.9, 1.2] }, + }, + + twilight: { + color() { return [new Color("021033"), new Color("16296b"), new Color("414791")] }, + position() { return [0, 0.5, 1] }, + }, + + night: { + color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] }, + position() { return [-0.5, 0.2, 0.5, 1] }, + }, + } + + const sunrise = sunData.sunrise + const sunset = sunData.sunset + + // Use sunrise or sunset if we're within 30min of it. + if (closeTo(sunrise)<=15) { return gradient.sunrise } + if (closeTo(sunset)<=15) { return gradient.sunset } + + // In the 30min before/after, use dawn/twilight. + if (closeTo(sunrise)<=45 && utcTime < sunrise) { return gradient.dawn } + if (closeTo(sunset)<=45 && utcTime > sunset) { return gradient.twilight } + + // Otherwise, if it's night, return night. + if (isNight(currentDate)) { return gradient.night } + + // If it's around noon, the sun is high in the sky. + if (currentDate.getHours() == 12) { return gradient.noon } + + // Otherwise, return the "typical" theme. + return gradient.midday +} + +// Set up the locationData object. +async function setupLocation() { + + locationData = {} + const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc") + + // If our location is unlocked or cache doesn't exist, ask iOS for location. + var readLocationFromFile = false + if (!lockLocation || !files.fileExists(locationPath)) { + try { + const location = await Location.current() + const geocode = await Location.reverseGeocode(location.latitude, location.longitude, locale) + locationData.latitude = location.latitude + locationData.longitude = location.longitude + locationData.locality = geocode[0].locality + files.writeString(locationPath, location.latitude + "|" + location.longitude + "|" + locationData.locality) + + } catch(e) { + // If we fail in unlocked mode, read it from the cache. + if (!lockLocation) { readLocationFromFile = true } + + // We can't recover if we fail on first run in locked mode. + else { return } + } + } + + // If our location is locked or we need to read from file, do it. + if (lockLocation || readLocationFromFile) { + const locationStr = files.readString(locationPath).split("|") + locationData.latitude = locationStr[0] + locationData.longitude = locationStr[1] + locationData.locality = locationStr[2] + } +} + +// Set up the sunData object. +async function setupSunrise() { + + // Requirements: location + if (!locationData) { await setupLocation() } + + // Set up the sunrise/sunset cache. + const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sun") + const sunCacheExists = files.fileExists(sunCachePath) + const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0 + let sunDataRaw, afterSunset + + // If cache exists and was created today, use cached data. + if (sunCacheExists && sameDay(currentDate, sunCacheDate)) { + const sunCache = files.readString(sunCachePath) + sunDataRaw = JSON.parse(sunCache) + + // Determine if it's after sunset. + const sunsetDate = new Date(sunDataRaw.results.sunset) + afterSunset = currentDate.getTime() - sunsetDate.getTime() > (45 * 60 * 1000) + } + + // If we don't have data yet, or we need to get tomorrow's data, get it from the server. + if (!sunDataRaw || afterSunset) { + let tomorrowDate = new Date() + tomorrowDate.setDate(currentDate.getDate() + 1) + const dateToUse = afterSunset ? tomorrowDate : currentDate + const sunReq = "https://api.sunrise-sunset.org/json?lat=" + locationData.latitude + "&lng=" + locationData.longitude + "&formatted=0&date=" + dateToUse.getFullYear() + "-" + (dateToUse.getMonth()+1) + "-" + dateToUse.getDate() + sunDataRaw = await new Request(sunReq).loadJSON() + files.writeString(sunCachePath, JSON.stringify(sunDataRaw)) + } + + // Store the timing values. + sunData = {} + sunData.sunrise = new Date(sunDataRaw.results.sunrise).getTime() + sunData.sunset = new Date(sunDataRaw.results.sunset).getTime() +} + +// Set up the weatherData object. +async function setupWeather() { + + // Requirements: location + if (!locationData) { await setupLocation() } + + // Set up the cache. + const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache") + const cacheExists = files.fileExists(cachePath) + const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0 + var weatherDataRaw + + // If cache exists and it's been less than 60 seconds since last request, use cached data. + if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) { + const cache = files.readString(cachePath) + weatherDataRaw = JSON.parse(cache) + + // Otherwise, use the API to get new weather data. + } else { + const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + locationData.latitude + "&lon=" + locationData.longitude + "&exclude=minutely,alerts&units=" + weatherSettings.units + "&lang=" + locale + "&appid=" + apiKey + weatherDataRaw = await new Request(weatherReq).loadJSON() + files.writeString(cachePath, JSON.stringify(weatherDataRaw)) + } + + // Store the weather values. + weatherData = {} + weatherData.currentTemp = weatherDataRaw.current.temp + weatherData.currentCondition = weatherDataRaw.current.weather[0].id + weatherData.currentDescription = weatherDataRaw.current.weather[0].main + weatherData.todayHigh = weatherDataRaw.daily[0].temp.max + weatherData.todayLow = weatherDataRaw.daily[0].temp.min + + weatherData.nextHourTemp = weatherDataRaw.hourly[1].temp + weatherData.nextHourCondition = weatherDataRaw.hourly[1].weather[0].id + + weatherData.tomorrowHigh = weatherDataRaw.daily[1].temp.max + weatherData.tomorrowLow = weatherDataRaw.daily[1].temp.min + weatherData.tomorrowCondition = weatherDataRaw.daily[1].weather[0].id +} + +/* + * WIDGET ITEMS + * These functions display items on the widget. + * ============================================ + */ + +// Display the date on the widget. +async function date(column) { + + // Requirements: events (if dynamicDateSize is enabled) + if (!eventData && dateSettings.dynamicDateSize) { await setupEvents() } + + // Set up the date formatter and set its locale. + let df = new DateFormatter() + df.locale = locale + + // Show small if it's hard coded, or if it's dynamic and events are visible. + if (dateSettings.staticDateSize == "small" || (dateSettings.dynamicDateSize && eventData.eventsAreVisible)) { + let dateStack = align(column) + dateStack.setPadding(padding, padding, padding, padding) + + df.dateFormat = dateSettings.smallDateFormat + let dateText = provideText(df.string(currentDate), dateStack, textFormat.smallDate) + + // Otherwise, show the large date. + } else { + let dateOneStack = align(column) + df.dateFormat = dateSettings.largeDateLineOne + let dateOne = provideText(df.string(currentDate), dateOneStack, textFormat.largeDate1) + dateOneStack.setPadding(padding/2, padding, 0, padding) + + let dateTwoStack = align(column) + df.dateFormat = dateSettings.largeDateLineTwo + let dateTwo = provideText(df.string(currentDate), dateTwoStack, textFormat.largeDate2) + dateTwoStack.setPadding(0, padding, padding, padding) + } +} + +// Display a time-based greeting on the widget. +async function greeting(column) { + + // This function makes a greeting based on the time of day. + function makeGreeting() { + const hour = currentDate.getHours() + if (hour < 5) { return localizedText.nightGreeting } + if (hour < 12) { return localizedText.morningGreeting } + if (hour-12 < 5) { return localizedText.afternoonGreeting } + if (hour-12 < 10) { return localizedText.eveningGreeting } + return localizedText.nightGreeting + } + + // Set up the greeting. + let greetingStack = align(column) + let greeting = provideText(makeGreeting(), greetingStack, textFormat.greeting) + greetingStack.setPadding(padding, padding, padding, padding) +} + +// Display events on the widget. +async function events(column) { + + // Requirements: events + if (!eventData) { await setupEvents() } + + // If no events are visible, figure out what to do. + if (!eventData.eventsAreVisible) { + const display = eventSettings.noEventBehavior + + // If it's a greeting, let the greeting function handle it. + if (display == "greeting") { return await greeting(column) } + + // If it's a message, get the localized text. + if (display == "message" && localizedText.noEventMessage.length) { + const messageStack = align(column) + messageStack.setPadding(padding, padding, padding, padding) + provideText(localizedText.noEventMessage, messageStack, textFormat.noEvents) + } + + // Whether or not we displayed something, return here. + return + } + + // Set up the event stack. + let eventStack = column.addStack() + eventStack.layoutVertically() + const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200 + eventStack.url = 'calshow:' + todaySeconds + + // If there are no events and we have a message, show it and return. + if (!eventData.eventsAreVisible && localizedText.noEventMessage.length) { + let message = provideText(localizedText.noEventMessage, eventStack, textFormat.noEvents) + eventStack.setPadding(padding, padding, padding, padding) + return + } + + // If we're not showing the message, don't pad the event stack. + eventStack.setPadding(0, 0, 0, 0) + + // Add each event to the stack. + var currentStack = eventStack + const futureEvents = eventData.futureEvents + for (let i = 0; i < futureEvents.length; i++) { + + const event = futureEvents[i] + const bottomPadding = (padding-10 < 0) ? 0 : padding-10 + + // If it's the tomorrow label, change to the tomorrow stack. + if (event.isLabel) { + let tomorrowStack = column.addStack() + tomorrowStack.layoutVertically() + const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800 + tomorrowStack.url = 'calshow:' + tomorrowSeconds + currentStack = tomorrowStack + + // Mimic the formatting of an event title, mostly. + const eventLabelStack = align(currentStack) + const eventLabel = provideText(event.title, eventLabelStack, textFormat.eventLabel) + eventLabelStack.setPadding(padding, padding, padding, padding) + continue + } + + const titleStack = align(currentStack) + titleStack.layoutHorizontally() + const showCalendarColor = eventSettings.showCalendarColor + const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle" + + // If we're showing a color, and it's not shown on the right, add it to the left. + if (showCalendarColor.length && !showCalendarColor.includes("right")) { + let colorItemText = provideTextSymbol(colorShape) + " " + let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) + colorItem.textColor = event.calendar.color + } + + const title = provideText(event.title.trim(), titleStack, textFormat.eventTitle) + titleStack.setPadding(padding, padding, event.isAllDay ? padding : padding/5, padding) + + // If we're showing a color on the right, show it. + if (showCalendarColor.length && showCalendarColor.includes("right")) { + let colorItemText = " " + provideTextSymbol(colorShape) + let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) + colorItem.textColor = event.calendar.color + } + + // If there are too many events, limit the line height. + if (futureEvents.length >= 3) { title.lineLimit = 1 } + + // If it's an all-day event, we don't need a time. + if (event.isAllDay) { continue } + + // Format the time information. + let timeText = formatTime(event.startDate) + + // If we show the length as time, add an en dash and the time. + if (eventSettings.showEventLength == "time") { + timeText += "–" + formatTime(event.endDate) + + // If we should it as a duration, add the minutes. + } else if (eventSettings.showEventLength == "duration") { + const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60) + const hours = Math.floor(duration/60) + const minutes = Math.floor(duration % 60) + const hourText = hours>0 ? hours + localizedText.durationHour : "" + const minuteText = minutes>0 ? minutes + localizedText.durationMinute : "" + const showSpace = hourText.length && minuteText.length + timeText += " \u2022 " + hourText + (showSpace ? " " : "") + minuteText + } + + const timeStack = align(currentStack) + const time = provideText(timeText, timeStack, textFormat.eventTime) + timeStack.setPadding(0, padding, padding, padding) + } +} + +// Display the current weather. +async function current(column) { + + // Requirements: weather and sunrise + if (!weatherData) { await setupWeather() } + if (!sunData) { await setupSunrise() } + + // Set up the current weather stack. + let currentWeatherStack = column.addStack() + currentWeatherStack.layoutVertically() + currentWeatherStack.setPadding(0, 0, 0, 0) + currentWeatherStack.url = "https://weather.com/weather/today/l/" + locationData.latitude + "," + locationData.longitude + + // If we're showing the location, add it. + if (weatherSettings.showLocation) { + let locationTextStack = align(currentWeatherStack) + let locationText = provideText(locationData.locality, locationTextStack, textFormat.smallTemp) + locationTextStack.setPadding(padding, padding, padding, padding) + } + + // Show the current condition symbol. + let mainConditionStack = align(currentWeatherStack) + let mainCondition = mainConditionStack.addImage(provideConditionSymbol(weatherData.currentCondition,isNight(currentDate))) + mainCondition.imageSize = new Size(22,22) + mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : padding, padding, 0, padding) + + // If we're showing the description, add it. + if (weatherSettings.showCondition) { + let conditionTextStack = align(currentWeatherStack) + let conditionText = provideText(weatherData.currentDescription, conditionTextStack, textFormat.smallTemp) + conditionTextStack.setPadding(padding, padding, 0, padding) + } + + // Show the current temperature. + const tempStack = align(currentWeatherStack) + tempStack.setPadding(0, padding, 0, padding) + const tempText = Math.round(weatherData.currentTemp) + "°" + const temp = provideText(tempText, tempStack, textFormat.largeTemp) + + // If we're not showing the high and low, end it here. + if (!weatherSettings.showHighLow) { return } + + // Show the temp bar and high/low values. + let tempBarStack = align(currentWeatherStack) + tempBarStack.layoutVertically() + tempBarStack.setPadding(0, padding, padding, padding) + + let tempBar = drawTempBar() + let tempBarImage = tempBarStack.addImage(tempBar) + tempBarImage.size = new Size(50,0) + + tempBarStack.addSpacer(1) + + let highLowStack = tempBarStack.addStack() + highLowStack.layoutHorizontally() + + const mainLowText = Math.round(weatherData.todayLow).toString() + const mainLow = provideText(mainLowText, highLowStack, textFormat.tinyTemp) + highLowStack.addSpacer() + const mainHighText = Math.round(weatherData.todayHigh).toString() + const mainHigh = provideText(mainHighText, highLowStack, textFormat.tinyTemp) + + tempBarStack.size = new Size(60,30) +} + +// Display upcoming weather. +async function future(column) { + + // Requirements: weather and sunrise + if (!weatherData) { await setupWeather() } + if (!sunData) { await setupSunrise() } + + // Set up the future weather stack. + let futureWeatherStack = column.addStack() + futureWeatherStack.layoutVertically() + futureWeatherStack.setPadding(0, 0, 0, 0) + futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + locationData.latitude + "," + locationData.longitude + + // Determine if we should show the next hour. + const showNextHour = (currentDate.getHours() < weatherSettings.tomorrowShownAtHour) + + // Set the label value. + const subLabelStack = align(futureWeatherStack) + const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel + const subLabel = provideText(subLabelText, subLabelStack, textFormat.smallTemp) + subLabelStack.setPadding(0, padding, padding/2, padding) + + // Set up the sub condition stack. + let subConditionStack = align(futureWeatherStack) + subConditionStack.layoutHorizontally() + subConditionStack.centerAlignContent() + subConditionStack.setPadding(0, padding, padding, padding) + + // Determine if it will be night in the next hour. + var nightCondition + if (showNextHour) { + const addHour = currentDate.getTime() + (60*60*1000) + const newDate = new Date(addHour) + nightCondition = isNight(newDate) + } else { + nightCondition = false + } + + let subCondition = subConditionStack.addImage(provideConditionSymbol(showNextHour ? weatherData.nextHourCondition : weatherData.tomorrowCondition,nightCondition)) + const subConditionSize = showNextHour ? 14 : 18 + subCondition.imageSize = new Size(subConditionSize, subConditionSize) + subConditionStack.addSpacer(5) + + // The next part of the display changes significantly for next hour vs tomorrow. + if (showNextHour) { + const subTempText = Math.round(weatherData.nextHourTemp) + "°" + const subTemp = provideText(subTempText, subConditionStack, textFormat.smallTemp) + + } else { + let tomorrowLine = subConditionStack.addImage(drawVerticalLine(new Color("ffffff", 0.5), 20)) + tomorrowLine.imageSize = new Size(3,28) + subConditionStack.addSpacer(5) + let tomorrowStack = subConditionStack.addStack() + tomorrowStack.layoutVertically() + + const tomorrowHighText = Math.round(weatherData.tomorrowHigh) + "" + const tomorrowHigh = provideText(tomorrowHighText, tomorrowStack, textFormat.tinyTemp) + tomorrowStack.addSpacer(4) + const tomorrowLowText = Math.round(weatherData.tomorrowLow) + "" + const tomorrowLow = provideText(tomorrowLowText, tomorrowStack, textFormat.tinyTemp) + } +} + +// Return a text-creation function. +function text(input = null) { + + function displayText(column) { + + // Don't do anything if the input is blank. + if (!input || input == "") { return } + + // Otherwise, add the text. + const textStack = align(column) + textStack.setPadding(padding, padding, padding, padding) + const textDisplay = provideText(input, textStack, textFormat.customText) + } + return displayText +} + +// Add a battery element to the widget; consisting of a battery icon and percentage. +async function battery(column) { + + // Get battery level via Scriptable function and format it in a convenient way + function getBatteryLevel() { + + const batteryLevel = Device.batteryLevel() + const batteryPercentage = `${Math.round(batteryLevel * 100)}%` + + return batteryPercentage + } + + const batteryLevel = Device.batteryLevel() + + // Set up the battery level item + let batteryStack = align(column) + batteryStack.layoutHorizontally() + batteryStack.centerAlignContent() + + let batteryIcon = batteryStack.addImage(provideBatteryIcon()) + batteryIcon.imageSize = new Size(30,30) + + // Change the battery icon to red if battery level is <= 20 to match system behavior + if ( Math.round(batteryLevel * 100) > 20 || Device.isCharging() ) { + + batteryIcon.tintColor = new Color(textFormat.battery.color || textFormat.defaultText.color) + + } else { + + batteryIcon.tintColor = Color.red() + + } + + batteryStack.addSpacer(padding * 0.6) + + // Display the battery status + let batteryInfo = provideText(getBatteryLevel(), batteryStack, textFormat.battery) + + batteryStack.setPadding(padding/2, padding, padding/2, padding) + +} + +// Show the sunrise or sunset time. +async function sunrise(column) { + + // Requirements: sunrise + if (!sunData) { await setupSunrise() } + + const sunrise = sunData.sunrise + const sunset = sunData.sunset + const showWithin = sunriseSettings.showWithin + const closeToSunrise = closeTo(sunrise) <= showWithin + const closeToSunset = closeTo(sunset) <= showWithin + + // If we only show sometimes and we're not close, return. + if (showWithin > 0 && !closeToSunrise && !closeToSunset) { return } + + // Otherwise, determine which time to show. + const showSunrise = closeTo(sunrise) <= closeTo(sunset) + + // Set up the stack. + const sunriseStack = align(column) + sunriseStack.setPadding(padding/2, padding, padding/2, padding) + sunriseStack.layoutHorizontally() + sunriseStack.centerAlignContent() + + sunriseStack.addSpacer(padding * 0.3) + + // Add the correct symbol. + const symbolName = showSunrise ? "sunrise.fill" : "sunset.fill" + const symbol = sunriseStack.addImage(SFSymbol.named(symbolName).image) + symbol.imageSize = new Size(22,22) + + sunriseStack.addSpacer(padding) + + // Add the time. + const timeText = formatTime(showSunrise ? new Date(sunrise) : new Date(sunset)) + const time = provideText(timeText, sunriseStack, textFormat.sunrise) +} + +// Allow for either term to be used. +async function sunset(column) { + return await sunrise(column) +} + +/* + * HELPER FUNCTIONS + * These functions perform duties for other functions. + * =================================================== + */ + +// Determines if the provided date is at night. +function isNight(dateInput) { + const timeValue = dateInput.getTime() + return (timeValue < sunData.sunrise) || (timeValue > sunData.sunset) +} + +// Determines if two dates occur on the same day +function sameDay(d1, d2) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() +} + +// Returns the number of minutes between now and the provided date. +function closeTo(time) { + return Math.abs(currentDate.getTime() - time) / 60000 +} + +// Format the time for a Date input. +function formatTime(date) { + let df = new DateFormatter() + df.locale = locale + df.useNoDateStyle() + df.useShortTimeStyle() + return df.string(date) +} + +// Provide a text symbol with the specified shape. +function provideTextSymbol(shape) { + + // Rectangle character. + if (shape.startsWith("rect")) { + return "\u2759" + } + // Circle character. + if (shape == "circle") { + return "\u2B24" + } + // Default to the rectangle. + return "\u2759" +} + +// Provide a battery SFSymbol with accurate level drawn on top of it. +function provideBatteryIcon() { + + // If we're charging, show the charging icon. + if (Device.isCharging()) { return SFSymbol.named("battery.100.bolt").image } + + // Set the size of the battery icon. + const batteryWidth = 87 + const batteryHeight = 41 + + // Start our draw context. + let draw = new DrawContext() + draw.opaque = false + draw.respectScreenScale = true + draw.size = new Size(batteryWidth, batteryHeight) + + // Draw the battery. + draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight)) + + // Match the battery level values to the SFSymbol. + const x = batteryWidth*0.1525 + const y = batteryHeight*0.247 + const width = batteryWidth*0.602 + const height = batteryHeight*0.505 + + // Prevent unreadable icons. + let level = Device.batteryLevel() + if (level < 0.05) { level = 0.05 } + + // Determine the width and radius of the battery level. + const current = width * level + let radius = height/6.5 + + // When it gets low, adjust the radius to match. + if (current < (radius * 2)) { radius = current / 2 } + + // Make the path for the battery level. + let barPath = new Path() + barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius) + draw.addPath(barPath) + draw.setFillColor(Color.black()) + draw.fillPath() + return draw.getImage() +} + +// Provide a symbol based on the condition. +function provideConditionSymbol(cond,night) { + + // Define our symbol equivalencies. + let symbols = { + + // Thunderstorm + "2": function() { return "cloud.bolt.rain.fill" }, + + // Drizzle + "3": function() { return "cloud.drizzle.fill" }, + + // Rain + "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" }, + + // Snow + "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" }, + + // Atmosphere + "7": function() { + if (cond == 781) { return "tornado" } + if (cond == 701 || cond == 741) { return "cloud.fog.fill" } + return night ? "cloud.fog.fill" : "sun.haze.fill" + }, + + // Clear and clouds + "8": function() { + if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" } + if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" } + return "cloud.fill" + } + } + + // Find out the first digit. + let conditionDigit = Math.floor(cond / 100) + + // Get the symbol. + return SFSymbol.named(symbols[conditionDigit]()).image +} + +// Provide a font based on the input. +function provideFont(fontName, fontSize) { + const fontGenerator = { + "ultralight": function() { return Font.ultraLightSystemFont(fontSize) }, + "light": function() { return Font.lightSystemFont(fontSize) }, + "regular": function() { return Font.regularSystemFont(fontSize) }, + "medium": function() { return Font.mediumSystemFont(fontSize) }, + "semibold": function() { return Font.semiboldSystemFont(fontSize) }, + "bold": function() { return Font.boldSystemFont(fontSize) }, + "heavy": function() { return Font.heavySystemFont(fontSize) }, + "black": function() { return Font.blackSystemFont(fontSize) }, + "italic": function() { return Font.italicSystemFont(fontSize) } + } + + const systemFont = fontGenerator[fontName] + if (systemFont) { return systemFont() } + return new Font(fontName, fontSize) +} + +// Add formatted text to a container. +function provideText(string, container, format) { + const textItem = container.addText(string) + const textFont = format.font || textFormat.defaultText.font + const textSize = format.size || textFormat.defaultText.size + const textColor = format.color || textFormat.defaultText.color + + textItem.font = provideFont(textFont, textSize) + textItem.textColor = new Color(textColor) + return textItem +} + +/* + * DRAWING FUNCTIONS + * These functions draw onto a canvas. + * =================================== + */ + +// Draw the vertical line in the tomorrow view. +function drawVerticalLine(color, height) { + + const width = 2 + + let draw = new DrawContext() + draw.opaque = false + draw.respectScreenScale = true + draw.size = new Size(width,height) + + let barPath = new Path() + const barHeight = height + barPath.addRoundedRect(new Rect(0, 0, width, height), width/2, width/2) + draw.addPath(barPath) + draw.setFillColor(color) + draw.fillPath() + + return draw.getImage() +} + +// Draw the temp bar. +function drawTempBar() { + + // Set the size of the temp bar. + const tempBarWidth = 200 + const tempBarHeight = 20 + + // Calculate the current percentage of the high-low range. + let percent = (weatherData.currentTemp - weatherData.todayLow) / (weatherData.todayHigh - weatherData.todayLow) + + // If we're out of bounds, clip it. + if (percent < 0) { + percent = 0 + } else if (percent > 1) { + percent = 1 + } + + // Determine the scaled x-value for the current temp. + const currPosition = (tempBarWidth - tempBarHeight) * percent + + // Start our draw context. + let draw = new DrawContext() + draw.opaque = false + draw.respectScreenScale = true + draw.size = new Size(tempBarWidth, tempBarHeight) + + // Make the path for the bar. + let barPath = new Path() + const barHeight = tempBarHeight - 10 + barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2) + draw.addPath(barPath) + draw.setFillColor(new Color("ffffff", 0.5)) + draw.fillPath() + + // Make the path for the current temp indicator. + let currPath = new Path() + currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight)) + draw.addPath(currPath) + draw.setFillColor(new Color("ffffff", 1)) + draw.fillPath() + + return draw.getImage() +} From 68e84a69e4e40be67fe1aae88e55135dab2824b0 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 07:58:03 +0300 Subject: [PATCH 03/21] Delete westher-cal.js --- westher-cal.js | 1408 ------------------------------------------------ 1 file changed, 1408 deletions(-) delete mode 100644 westher-cal.js diff --git a/westher-cal.js b/westher-cal.js deleted file mode 100644 index 77224db..0000000 --- a/westher-cal.js +++ /dev/null @@ -1,1408 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: teal; icon-glyph: magic; -/* - * SETUP - * Use this section to set up the widget. - * ====================================== - */ - -// To use weather, get a free API key at openweathermap.org/appid and paste it in between the quotation marks. -const apiKey = "" - -// Set the locale code. Leave blank "" to match the device's locale. You can change the hard-coded text strings in the TEXT section below. -let locale = "en" - -// Set to true for fixed location, false to update location as you move around -const lockLocation = true - -// The size of the widget preview in the app. -const widgetPreview = "large" - -// Set to true for an image background, false for no image. -const imageBackground = true - -// Set to true to reset the widget's background image. -const forceImageUpdate = false - -// Set the padding around each item. Default is 5. -const padding = 5 - -/* - * LAYOUT - * Decide what items to show on the widget. - * ======================================== - */ - -// You always need to start with "row," and "column," items, but you can now add as many as you want. -// Adding left, right, or center will align everything after that. The default alignment is left. - -// You can add a flexible vertical space with "space," or a fixed-size space like this: "space(50)" -// Align items to the top or bottom of columns by adding "space," before or after all items in the column. - -// There are many possible items, including: date, greeting, events, current, future, battery, sunrise, and text("Your text here") -// Make sure to always put a comma after each item. - -const items = [ - - row, - - column, - date, - battery, - sunrise, - space, - - column(90), - current, - future, - - row, - - column, - events, - -] - -/* - * ITEM SETTINGS - * Choose how each item is displayed. - * ================================== - */ - -// DATE -// ==== -const dateSettings = { - - // If set to true, date will become smaller when events are displayed. - dynamicDateSize: false - - // If the date is not dynamic, should it be large or small? - ,staticDateSize: "large" - - // Determine the date format for each date type. See docs.scriptable.app/dateformatter - ,smallDateFormat: "EEEE, MMMM d" - ,largeDateLineOne: "EEEE," - ,largeDateLineTwo: "MMMM d" -} - -// EVENTS -// ====== -const eventSettings = { - - // How many events to show. - numberOfEvents: 3 - - // Show all-day events. - ,showAllDay: true - - // Show tomorrow's events. - ,showTomorrow: true - - // Can be blank "" or set to "duration" or "time" to display how long an event is. - ,showEventLength: "duration" - - // Set which calendars for which to show events. Empty [] means all calendars. - ,selectCalendars: [] - - // Leave blank "" for no color, or specify shape (circle, rectangle) and/or side (left, right). - ,showCalendarColor: "rectangle left" - - // When no events remain, show a hard-coded "message", a "greeting", or "none". - ,noEventBehavior: "message" -} - -// SUNRISE -// ======= -const sunriseSettings = { - - // How many minutes before/after sunrise or sunset to show this element. 0 for always. - showWithin: 0 -} - -// WEATHER -// ======= -const weatherSettings = { - - // Set to imperial for Fahrenheit, or metric for Celsius - units: "metric" - - // Show the location of the current weather. - ,showLocation: false - - // Show the text description of the current conditions. - ,showCondition: false - - // Show today's high and low temperatures. - ,showHighLow: true - - // Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it. - ,tomorrowShownAtHour: 20 -} - -/* - * TEXT - * Change the language and formatting of text displayed. - * ===================================================== - */ - -// You can change the language or wording of any text in the widget. -const localizedText = { - - // The text shown if you add a greeting item to the layout. - nightGreeting: "Good night." - ,morningGreeting: "Good morning." - ,afternoonGreeting: "Good afternoon." - ,eveningGreeting: "Good evening." - - // The text shown if you add a future weather item to the layout, or tomorrow's events. - ,nextHourLabel: "Next hour" - ,tomorrowLabel: "Tomorrow" - - // Shown when noEventBehavior is set to "message". - ,noEventMessage: "Enjoy the rest of your day." - - // The text shown after the hours and minutes of an event duration. - ,durationMinute: "m" - ,durationHour: "h" - -} - -// Set the font, size, and color of various text elements. Use iosfonts.com to find fonts to use. If you want to use the default iOS font, set the font name to one of the following: ultralight, light, regular, medium, semibold, bold, heavy, black, or italic. -const textFormat = { - - // Set the default font and color. - defaultText: { size: 14, color: "ffffff", font: "regular" }, - - // Any blank values will use the default. - smallDate: { size: 17, color: "", font: "semibold" }, - largeDate1: { size: 30, color: "", font: "light" }, - largeDate2: { size: 30, color: "", font: "light" }, - - greeting: { size: 30, color: "", font: "semibold" }, - eventLabel: { size: 14, color: "", font: "semibold" }, - eventTitle: { size: 14, color: "", font: "semibold" }, - eventTime: { size: 14, color: "ffffffcc", font: "" }, - noEvents: { size: 30, color: "", font: "semibold" }, - - largeTemp: { size: 34, color: "", font: "light" }, - smallTemp: { size: 14, color: "", font: "" }, - tinyTemp: { size: 12, color: "", font: "" }, - - customText: { size: 14, color: "", font: "" }, - - battery: { size: 14, color: "", font: "medium" }, - sunrise: { size: 14, color: "", font: "medium" }, -} - -/* - * WIDGET CODE - * Be more careful editing this section. - * ===================================== - */ - -// Make sure we have a locale value. -if (locale == "" || locale == null) { locale = Device.locale() } - -// Declare the data variables. -var eventData, locationData, sunData, weatherData - -// Create global constants. -const currentDate = new Date() -const files = FileManager.local() - -/* - * CONSTRUCTION - * ============ - */ - -// Set up the widget with padding. -const widget = new ListWidget() -const horizontalPad = padding < 10 ? 10 - padding : 10 -const verticalPad = padding < 15 ? 15 - padding : 15 -widget.setPadding(horizontalPad, verticalPad, horizontalPad, verticalPad) -widget.spacing = 0 - -// Set up the global variables. -var currentRow = {} -var currentColumn = {} - -// Set up the initial alignment. -var currentAlignment = alignLeft - -// Set up the global ASCII variables. -var currentColumns = [] -var rowNeedsSetup = false - -// It's ASCII time! -if (typeof items[0] == 'string') { - for (line of items[0].split(/\r?\n/)) { await processLine(line) } -} -// Otherwise, set up normally. -else { - for (item of items) { await item(currentColumn) } -} - -/* - * BACKGROUND DISPLAY - * ================== - */ - -// If it's an image background, display it. -if (imageBackground) { - - // Determine if our image exists and when it was saved. - const path = files.joinPath(files.documentsDirectory(), "weather-cal-image") - const exists = files.fileExists(path) - - // If it exists and an update isn't forced, use the cache. - if (exists && (config.runsInWidget || !forceImageUpdate)) { - widget.backgroundImage = files.readImage(path) - - // If it's missing when running in the widget, use a gray background. - } else if (!exists && config.runsInWidget) { - widget.backgroundColor = Color.gray() - - // But if we're running in app, prompt the user for the image. - } else { - const img = await Photos.fromLibrary() - widget.backgroundImage = img - files.writeImage(path, img) - } - -// If it's not an image background, show the gradient. -} else { - let gradient = new LinearGradient() - let gradientSettings = await setupGradient() - - gradient.colors = gradientSettings.color() - gradient.locations = gradientSettings.position() - - widget.backgroundGradient = gradient -} - -// Finish the widget and show a preview. -Script.setWidget(widget) -if (widgetPreview == "small") { widget.presentSmall() } -else if (widgetPreview == "medium") { widget.presentMedium() } -else if (widgetPreview == "large") { widget.presentLarge() } -Script.complete() - -/* - * ASCII FUNCTIONS - * Now isn't this a lot of fun? - * ============================ - */ - -// Provide the named function. -function provideFunction(name) { - const functions = { - space() { return space }, - left() { return left }, - right() { return right }, - center() { return center }, - date() { return date }, - greeting() { return greeting }, - events() { return events }, - current() { return current }, - future() { return future }, - battery() { return battery }, - sunrise() { return sunrise }, - } - return functions[name] -} - -// Processes a single line of ASCII. -async function processLine(lineInput) { - - // Because iOS loves adding periods to everything. - const line = lineInput.replace(/\.+/g,'') - - // If it's blank, return. - if (line.trim() == '') { return } - - // If it's a line, enumerate previous columns (if any) and set up the new row. - if (line[0] == '-' && line[line.length-1] == '-') { - if (currentColumns.length > 0) { await enumerateColumns() } - rowNeedsSetup = true - return - } - - // If it's the first content row, finish the row setup. - if (rowNeedsSetup) { - row(currentColumn) - rowNeedsSetup = false - } - - // If there's a number, this is a setup row. - const setupRow = line.match(/\d+/) - - // Otherwise, it has columns. - const items = line.split('|') - - // Iterate through each item. - for (var i=1; i < items.length-1; i++) { - - // If the current column doesn't exist, make it. - if (!currentColumns[i]) { currentColumns[i] = { items: [] } } - - // Now we have a column to add the items to. - const column = currentColumns[i].items - - // Get the current item and its trimmed version. - const item = items[i] - const trim = item.trim() - - // If it's not a function, figure out spacing. - if (!provideFunction(trim)) { - - // If it's a setup row, whether or not we find the number, we keep going. - if (setupRow) { - const value = parseInt(trim, 10) - if (value) { currentColumns[i].width = value } - continue - } - - // If it's blank and we haven't already added a space, add one. - const prevItem = column[column.length-1] - if (trim == '' && (!prevItem || (prevItem && !prevItem.startsWith("space")))) { - column.push("space") - } - - // Either way, we're done. - continue - - } - - // Determine the alignment. - const index = item.indexOf(trim) - const length = item.slice(index,item.length).length - - let align - if (index > 0 && length > trim.length) { align = "center" } - else if (index > 0) { align = "right" } - else { align = "left" } - - // Add the items to the column. - column.push(align) - column.push(trim) - } -} - -// Runs the function names in each column. -async function enumerateColumns() { - if (currentColumns.length > 0) { - for (col of currentColumns) { - - // If it's null, go to the next one. - if (!col) { continue } - - // If there's a width, use the width function. - if (col.width) { - column(col.width)(currentColumn) - - // Otherwise, create the column normally. - } else { - column(currentColumn) - } - for (item of col.items) { - const func = provideFunction(item)() - await func(currentColumn) - } - } - currentColumns = [] - } -} - -/* - * LAYOUT FUNCTIONS - * These functions manage spacing and alignment. - * ============================================= - */ - -// Makes a new row on the widget. -function row(input = null) { - - function makeRow() { - currentRow = widget.addStack() - currentRow.layoutHorizontally() - currentRow.setPadding(0, 0, 0, 0) - currentColumn.spacing = 0 - - // If input was given, make a column of that size. - if (input > 0) { currentRow.size = new Size(0,input) } - } - - // If there's no input or it's a number, it's being called in the layout declaration. - if (!input || typeof input == "number") { return makeRow } - - // Otherwise, it's being called in the generator. - else { makeRow() } -} - -// Makes a new column on the widget. -function column(input = null) { - - function makeColumn() { - currentColumn = currentRow.addStack() - currentColumn.layoutVertically() - currentColumn.setPadding(0, 0, 0, 0) - currentColumn.spacing = 0 - - // If input was given, make a column of that size. - if (input > 0) { currentColumn.size = new Size(input,0) } - } - - // If there's no input or it's a number, it's being called in the layout declaration. - if (!input || typeof input == "number") { return makeColumn } - - // Otherwise, it's being called in the generator. - else { makeColumn() } -} - -// Create an aligned stack to add content to. -function align(column) { - - // Add the containing stack to the column. - let alignmentStack = column.addStack() - alignmentStack.layoutHorizontally() - - // Get the correct stack from the alignment function. - let returnStack = currentAlignment(alignmentStack) - returnStack.layoutVertically() - return returnStack -} - -// Create a right-aligned stack. -function alignRight(alignmentStack) { - alignmentStack.addSpacer() - let returnStack = alignmentStack.addStack() - return returnStack -} - -// Create a left-aligned stack. -function alignLeft(alignmentStack) { - let returnStack = alignmentStack.addStack() - alignmentStack.addSpacer() - return returnStack -} - -// Create a center-aligned stack. -function alignCenter(alignmentStack) { - alignmentStack.addSpacer() - let returnStack = alignmentStack.addStack() - alignmentStack.addSpacer() - return returnStack -} - -// This function adds a space, with an optional amount. -function space(input = null) { - - // This function adds a spacer with the input width. - function spacer(column) { - - // If the input is null or zero, add a flexible spacer. - if (!input || input == 0) { column.addSpacer() } - - // Otherwise, add a space with the specified length. - else { column.addSpacer(input) } - } - - // If there's no input or it's a number, it's being called in the column declaration. - if (!input || typeof input == "number") { return spacer } - - // Otherwise, it's being called in the column generator. - else { input.addSpacer() } -} - -// Change the current alignment to right. -function right(x) { currentAlignment = alignRight } - -// Change the current alignment to left. -function left(x) { currentAlignment = alignLeft } - -// Change the current alignment to center. -function center(x) { currentAlignment = alignCenter } - -/* - * SETUP FUNCTIONS - * These functions prepare data needed for items. - * ============================================== - */ - -// Set up the eventData object. -async function setupEvents() { - - eventData = {} - const calendars = eventSettings.selectCalendars - const numberOfEvents = eventSettings.numberOfEvents - - // Function to determine if an event should be shown. - function shouldShowEvent(event) { - - // If events are filtered and the calendar isn't in the selected calendars, return false. - if (calendars.length && !calendars.includes(event.calendar.title)) { return false } - - // Hack to remove canceled Office 365 events. - if (event.title.startsWith("Canceled:")) { return false } - - // If it's an all-day event, only show if the setting is active. - if (event.isAllDay) { return eventSettings.showAllDay } - - // Otherwise, return the event if it's in the future. - return (event.startDate.getTime() > currentDate.getTime()) - } - - // Determine which events to show, and how many. - const todayEvents = await CalendarEvent.today([]) - let shownEvents = 0 - let futureEvents = [] - - for (const event of todayEvents) { - if (shownEvents == numberOfEvents) { break } - if (shouldShowEvent(event)) { - futureEvents.push(event) - shownEvents++ - } - } - - // If there's room and we need to, show tomorrow's events. - let multipleTomorrowEvents = false - if (eventSettings.showTomorrow && shownEvents < numberOfEvents) { - - const tomorrowEvents = await CalendarEvent.tomorrow([]) - for (const event of tomorrowEvents) { - if (shownEvents == numberOfEvents) { break } - if (shouldShowEvent(event)) { - - // Add the tomorrow label prior to the first tomorrow event. - if (!multipleTomorrowEvents) { - - // The tomorrow label is pretending to be an event. - futureEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isLabel: true }) - multipleTomorrowEvents = true - } - - // Show the tomorrow event and increment the counter. - futureEvents.push(event) - shownEvents++ - } - } - } - - // Store the future events, and whether or not any events are displayed. - eventData.futureEvents = futureEvents - eventData.eventsAreVisible = (futureEvents.length > 0) && (eventSettings.numberOfEvents > 0) -} - -// Set up the gradient for the widget background. -async function setupGradient() { - - // Requirements: sunrise - if (!sunData) { await setupSunrise() } - - let gradient = { - dawn: { - color() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] }, - position() { return [0, 0.5, 1] }, - }, - - sunrise: { - color() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] }, - position() { return [0, 0.8, 1.5] }, - }, - - midday: { - color() { return [new Color("3a8cc1"), new Color("90c0df")] }, - position() { return [0, 1] }, - }, - - noon: { - color() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] }, - position() { return [-0.2, 0.2, 1.5] }, - }, - - sunset: { - color() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] }, - position() { return [0.1, 0.9, 1.2] }, - }, - - twilight: { - color() { return [new Color("021033"), new Color("16296b"), new Color("414791")] }, - position() { return [0, 0.5, 1] }, - }, - - night: { - color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] }, - position() { return [-0.5, 0.2, 0.5, 1] }, - }, - } - - const sunrise = sunData.sunrise - const sunset = sunData.sunset - - // Use sunrise or sunset if we're within 30min of it. - if (closeTo(sunrise)<=15) { return gradient.sunrise } - if (closeTo(sunset)<=15) { return gradient.sunset } - - // In the 30min before/after, use dawn/twilight. - if (closeTo(sunrise)<=45 && utcTime < sunrise) { return gradient.dawn } - if (closeTo(sunset)<=45 && utcTime > sunset) { return gradient.twilight } - - // Otherwise, if it's night, return night. - if (isNight(currentDate)) { return gradient.night } - - // If it's around noon, the sun is high in the sky. - if (currentDate.getHours() == 12) { return gradient.noon } - - // Otherwise, return the "typical" theme. - return gradient.midday -} - -// Set up the locationData object. -async function setupLocation() { - - locationData = {} - const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc") - - // If our location is unlocked or cache doesn't exist, ask iOS for location. - var readLocationFromFile = false - if (!lockLocation || !files.fileExists(locationPath)) { - try { - const location = await Location.current() - const geocode = await Location.reverseGeocode(location.latitude, location.longitude, locale) - locationData.latitude = location.latitude - locationData.longitude = location.longitude - locationData.locality = geocode[0].locality - files.writeString(locationPath, location.latitude + "|" + location.longitude + "|" + locationData.locality) - - } catch(e) { - // If we fail in unlocked mode, read it from the cache. - if (!lockLocation) { readLocationFromFile = true } - - // We can't recover if we fail on first run in locked mode. - else { return } - } - } - - // If our location is locked or we need to read from file, do it. - if (lockLocation || readLocationFromFile) { - const locationStr = files.readString(locationPath).split("|") - locationData.latitude = locationStr[0] - locationData.longitude = locationStr[1] - locationData.locality = locationStr[2] - } -} - -// Set up the sunData object. -async function setupSunrise() { - - // Requirements: location - if (!locationData) { await setupLocation() } - - // Set up the sunrise/sunset cache. - const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sun") - const sunCacheExists = files.fileExists(sunCachePath) - const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0 - let sunDataRaw, afterSunset - - // If cache exists and was created today, use cached data. - if (sunCacheExists && sameDay(currentDate, sunCacheDate)) { - const sunCache = files.readString(sunCachePath) - sunDataRaw = JSON.parse(sunCache) - - // Determine if it's after sunset. - const sunsetDate = new Date(sunDataRaw.results.sunset) - afterSunset = currentDate.getTime() - sunsetDate.getTime() > (45 * 60 * 1000) - } - - // If we don't have data yet, or we need to get tomorrow's data, get it from the server. - if (!sunDataRaw || afterSunset) { - let tomorrowDate = new Date() - tomorrowDate.setDate(currentDate.getDate() + 1) - const dateToUse = afterSunset ? tomorrowDate : currentDate - const sunReq = "https://api.sunrise-sunset.org/json?lat=" + locationData.latitude + "&lng=" + locationData.longitude + "&formatted=0&date=" + dateToUse.getFullYear() + "-" + (dateToUse.getMonth()+1) + "-" + dateToUse.getDate() - sunDataRaw = await new Request(sunReq).loadJSON() - files.writeString(sunCachePath, JSON.stringify(sunDataRaw)) - } - - // Store the timing values. - sunData = {} - sunData.sunrise = new Date(sunDataRaw.results.sunrise).getTime() - sunData.sunset = new Date(sunDataRaw.results.sunset).getTime() -} - -// Set up the weatherData object. -async function setupWeather() { - - // Requirements: location - if (!locationData) { await setupLocation() } - - // Set up the cache. - const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache") - const cacheExists = files.fileExists(cachePath) - const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0 - var weatherDataRaw - - // If cache exists and it's been less than 60 seconds since last request, use cached data. - if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) { - const cache = files.readString(cachePath) - weatherDataRaw = JSON.parse(cache) - - // Otherwise, use the API to get new weather data. - } else { - const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + locationData.latitude + "&lon=" + locationData.longitude + "&exclude=minutely,alerts&units=" + weatherSettings.units + "&lang=" + locale + "&appid=" + apiKey - weatherDataRaw = await new Request(weatherReq).loadJSON() - files.writeString(cachePath, JSON.stringify(weatherDataRaw)) - } - - // Store the weather values. - weatherData = {} - weatherData.currentTemp = weatherDataRaw.current.temp - weatherData.currentCondition = weatherDataRaw.current.weather[0].id - weatherData.currentDescription = weatherDataRaw.current.weather[0].main - weatherData.todayHigh = weatherDataRaw.daily[0].temp.max - weatherData.todayLow = weatherDataRaw.daily[0].temp.min - - weatherData.nextHourTemp = weatherDataRaw.hourly[1].temp - weatherData.nextHourCondition = weatherDataRaw.hourly[1].weather[0].id - - weatherData.tomorrowHigh = weatherDataRaw.daily[1].temp.max - weatherData.tomorrowLow = weatherDataRaw.daily[1].temp.min - weatherData.tomorrowCondition = weatherDataRaw.daily[1].weather[0].id -} - -/* - * WIDGET ITEMS - * These functions display items on the widget. - * ============================================ - */ - -// Display the date on the widget. -async function date(column) { - - // Requirements: events (if dynamicDateSize is enabled) - if (!eventData && dateSettings.dynamicDateSize) { await setupEvents() } - - // Set up the date formatter and set its locale. - let df = new DateFormatter() - df.locale = locale - - // Show small if it's hard coded, or if it's dynamic and events are visible. - if (dateSettings.staticDateSize == "small" || (dateSettings.dynamicDateSize && eventData.eventsAreVisible)) { - let dateStack = align(column) - dateStack.setPadding(padding, padding, padding, padding) - - df.dateFormat = dateSettings.smallDateFormat - let dateText = provideText(df.string(currentDate), dateStack, textFormat.smallDate) - - // Otherwise, show the large date. - } else { - let dateOneStack = align(column) - df.dateFormat = dateSettings.largeDateLineOne - let dateOne = provideText(df.string(currentDate), dateOneStack, textFormat.largeDate1) - dateOneStack.setPadding(padding/2, padding, 0, padding) - - let dateTwoStack = align(column) - df.dateFormat = dateSettings.largeDateLineTwo - let dateTwo = provideText(df.string(currentDate), dateTwoStack, textFormat.largeDate2) - dateTwoStack.setPadding(0, padding, padding, padding) - } -} - -// Display a time-based greeting on the widget. -async function greeting(column) { - - // This function makes a greeting based on the time of day. - function makeGreeting() { - const hour = currentDate.getHours() - if (hour < 5) { return localizedText.nightGreeting } - if (hour < 12) { return localizedText.morningGreeting } - if (hour-12 < 5) { return localizedText.afternoonGreeting } - if (hour-12 < 10) { return localizedText.eveningGreeting } - return localizedText.nightGreeting - } - - // Set up the greeting. - let greetingStack = align(column) - let greeting = provideText(makeGreeting(), greetingStack, textFormat.greeting) - greetingStack.setPadding(padding, padding, padding, padding) -} - -// Display events on the widget. -async function events(column) { - - // Requirements: events - if (!eventData) { await setupEvents() } - - // If no events are visible, figure out what to do. - if (!eventData.eventsAreVisible) { - const display = eventSettings.noEventBehavior - - // If it's a greeting, let the greeting function handle it. - if (display == "greeting") { return await greeting(column) } - - // If it's a message, get the localized text. - if (display == "message" && localizedText.noEventMessage.length) { - const messageStack = align(column) - messageStack.setPadding(padding, padding, padding, padding) - provideText(localizedText.noEventMessage, messageStack, textFormat.noEvents) - } - - // Whether or not we displayed something, return here. - return - } - - // Set up the event stack. - let eventStack = column.addStack() - eventStack.layoutVertically() - const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200 - eventStack.url = 'calshow:' + todaySeconds - - // If there are no events and we have a message, show it and return. - if (!eventData.eventsAreVisible && localizedText.noEventMessage.length) { - let message = provideText(localizedText.noEventMessage, eventStack, textFormat.noEvents) - eventStack.setPadding(padding, padding, padding, padding) - return - } - - // If we're not showing the message, don't pad the event stack. - eventStack.setPadding(0, 0, 0, 0) - - // Add each event to the stack. - var currentStack = eventStack - const futureEvents = eventData.futureEvents - for (let i = 0; i < futureEvents.length; i++) { - - const event = futureEvents[i] - const bottomPadding = (padding-10 < 0) ? 0 : padding-10 - - // If it's the tomorrow label, change to the tomorrow stack. - if (event.isLabel) { - let tomorrowStack = column.addStack() - tomorrowStack.layoutVertically() - const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800 - tomorrowStack.url = 'calshow:' + tomorrowSeconds - currentStack = tomorrowStack - - // Mimic the formatting of an event title, mostly. - const eventLabelStack = align(currentStack) - const eventLabel = provideText(event.title, eventLabelStack, textFormat.eventLabel) - eventLabelStack.setPadding(padding, padding, padding, padding) - continue - } - - const titleStack = align(currentStack) - titleStack.layoutHorizontally() - const showCalendarColor = eventSettings.showCalendarColor - const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle" - - // If we're showing a color, and it's not shown on the right, add it to the left. - if (showCalendarColor.length && !showCalendarColor.includes("right")) { - let colorItemText = provideTextSymbol(colorShape) + " " - let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) - colorItem.textColor = event.calendar.color - } - - const title = provideText(event.title.trim(), titleStack, textFormat.eventTitle) - titleStack.setPadding(padding, padding, event.isAllDay ? padding : padding/5, padding) - - // If we're showing a color on the right, show it. - if (showCalendarColor.length && showCalendarColor.includes("right")) { - let colorItemText = " " + provideTextSymbol(colorShape) - let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) - colorItem.textColor = event.calendar.color - } - - // If there are too many events, limit the line height. - if (futureEvents.length >= 3) { title.lineLimit = 1 } - - // If it's an all-day event, we don't need a time. - if (event.isAllDay) { continue } - - // Format the time information. - let timeText = formatTime(event.startDate) - - // If we show the length as time, add an en dash and the time. - if (eventSettings.showEventLength == "time") { - timeText += "–" + formatTime(event.endDate) - - // If we should it as a duration, add the minutes. - } else if (eventSettings.showEventLength == "duration") { - const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60) - const hours = Math.floor(duration/60) - const minutes = Math.floor(duration % 60) - const hourText = hours>0 ? hours + localizedText.durationHour : "" - const minuteText = minutes>0 ? minutes + localizedText.durationMinute : "" - const showSpace = hourText.length && minuteText.length - timeText += " \u2022 " + hourText + (showSpace ? " " : "") + minuteText - } - - const timeStack = align(currentStack) - const time = provideText(timeText, timeStack, textFormat.eventTime) - timeStack.setPadding(0, padding, padding, padding) - } -} - -// Display the current weather. -async function current(column) { - - // Requirements: weather and sunrise - if (!weatherData) { await setupWeather() } - if (!sunData) { await setupSunrise() } - - // Set up the current weather stack. - let currentWeatherStack = column.addStack() - currentWeatherStack.layoutVertically() - currentWeatherStack.setPadding(0, 0, 0, 0) - currentWeatherStack.url = "https://weather.com/weather/today/l/" + locationData.latitude + "," + locationData.longitude - - // If we're showing the location, add it. - if (weatherSettings.showLocation) { - let locationTextStack = align(currentWeatherStack) - let locationText = provideText(locationData.locality, locationTextStack, textFormat.smallTemp) - locationTextStack.setPadding(padding, padding, padding, padding) - } - - // Show the current condition symbol. - let mainConditionStack = align(currentWeatherStack) - let mainCondition = mainConditionStack.addImage(provideConditionSymbol(weatherData.currentCondition,isNight(currentDate))) - mainCondition.imageSize = new Size(22,22) - mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : padding, padding, 0, padding) - - // If we're showing the description, add it. - if (weatherSettings.showCondition) { - let conditionTextStack = align(currentWeatherStack) - let conditionText = provideText(weatherData.currentDescription, conditionTextStack, textFormat.smallTemp) - conditionTextStack.setPadding(padding, padding, 0, padding) - } - - // Show the current temperature. - const tempStack = align(currentWeatherStack) - tempStack.setPadding(0, padding, 0, padding) - const tempText = Math.round(weatherData.currentTemp) + "°" - const temp = provideText(tempText, tempStack, textFormat.largeTemp) - - // If we're not showing the high and low, end it here. - if (!weatherSettings.showHighLow) { return } - - // Show the temp bar and high/low values. - let tempBarStack = align(currentWeatherStack) - tempBarStack.layoutVertically() - tempBarStack.setPadding(0, padding, padding, padding) - - let tempBar = drawTempBar() - let tempBarImage = tempBarStack.addImage(tempBar) - tempBarImage.size = new Size(50,0) - - tempBarStack.addSpacer(1) - - let highLowStack = tempBarStack.addStack() - highLowStack.layoutHorizontally() - - const mainLowText = Math.round(weatherData.todayLow).toString() - const mainLow = provideText(mainLowText, highLowStack, textFormat.tinyTemp) - highLowStack.addSpacer() - const mainHighText = Math.round(weatherData.todayHigh).toString() - const mainHigh = provideText(mainHighText, highLowStack, textFormat.tinyTemp) - - tempBarStack.size = new Size(60,30) -} - -// Display upcoming weather. -async function future(column) { - - // Requirements: weather and sunrise - if (!weatherData) { await setupWeather() } - if (!sunData) { await setupSunrise() } - - // Set up the future weather stack. - let futureWeatherStack = column.addStack() - futureWeatherStack.layoutVertically() - futureWeatherStack.setPadding(0, 0, 0, 0) - futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + locationData.latitude + "," + locationData.longitude - - // Determine if we should show the next hour. - const showNextHour = (currentDate.getHours() < weatherSettings.tomorrowShownAtHour) - - // Set the label value. - const subLabelStack = align(futureWeatherStack) - const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel - const subLabel = provideText(subLabelText, subLabelStack, textFormat.smallTemp) - subLabelStack.setPadding(0, padding, padding/2, padding) - - // Set up the sub condition stack. - let subConditionStack = align(futureWeatherStack) - subConditionStack.layoutHorizontally() - subConditionStack.centerAlignContent() - subConditionStack.setPadding(0, padding, padding, padding) - - // Determine if it will be night in the next hour. - var nightCondition - if (showNextHour) { - const addHour = currentDate.getTime() + (60*60*1000) - const newDate = new Date(addHour) - nightCondition = isNight(newDate) - } else { - nightCondition = false - } - - let subCondition = subConditionStack.addImage(provideConditionSymbol(showNextHour ? weatherData.nextHourCondition : weatherData.tomorrowCondition,nightCondition)) - const subConditionSize = showNextHour ? 14 : 18 - subCondition.imageSize = new Size(subConditionSize, subConditionSize) - subConditionStack.addSpacer(5) - - // The next part of the display changes significantly for next hour vs tomorrow. - if (showNextHour) { - const subTempText = Math.round(weatherData.nextHourTemp) + "°" - const subTemp = provideText(subTempText, subConditionStack, textFormat.smallTemp) - - } else { - let tomorrowLine = subConditionStack.addImage(drawVerticalLine(new Color("ffffff", 0.5), 20)) - tomorrowLine.imageSize = new Size(3,28) - subConditionStack.addSpacer(5) - let tomorrowStack = subConditionStack.addStack() - tomorrowStack.layoutVertically() - - const tomorrowHighText = Math.round(weatherData.tomorrowHigh) + "" - const tomorrowHigh = provideText(tomorrowHighText, tomorrowStack, textFormat.tinyTemp) - tomorrowStack.addSpacer(4) - const tomorrowLowText = Math.round(weatherData.tomorrowLow) + "" - const tomorrowLow = provideText(tomorrowLowText, tomorrowStack, textFormat.tinyTemp) - } -} - -// Return a text-creation function. -function text(input = null) { - - function displayText(column) { - - // Don't do anything if the input is blank. - if (!input || input == "") { return } - - // Otherwise, add the text. - const textStack = align(column) - textStack.setPadding(padding, padding, padding, padding) - const textDisplay = provideText(input, textStack, textFormat.customText) - } - return displayText -} - -// Add a battery element to the widget; consisting of a battery icon and percentage. -async function battery(column) { - - // Get battery level via Scriptable function and format it in a convenient way - function getBatteryLevel() { - - const batteryLevel = Device.batteryLevel() - const batteryPercentage = `${Math.round(batteryLevel * 100)}%` - - return batteryPercentage - } - - const batteryLevel = Device.batteryLevel() - - // Set up the battery level item - let batteryStack = align(column) - batteryStack.layoutHorizontally() - batteryStack.centerAlignContent() - - let batteryIcon = batteryStack.addImage(provideBatteryIcon()) - batteryIcon.imageSize = new Size(30,30) - - // Change the battery icon to red if battery level is <= 20 to match system behavior - if ( Math.round(batteryLevel * 100) > 20 || Device.isCharging() ) { - - batteryIcon.tintColor = new Color(textFormat.battery.color || textFormat.defaultText.color) - - } else { - - batteryIcon.tintColor = Color.red() - - } - - batteryStack.addSpacer(padding * 0.6) - - // Display the battery status - let batteryInfo = provideText(getBatteryLevel(), batteryStack, textFormat.battery) - - batteryStack.setPadding(padding/2, padding, padding/2, padding) - -} - -// Show the sunrise or sunset time. -async function sunrise(column) { - - // Requirements: sunrise - if (!sunData) { await setupSunrise() } - - const sunrise = sunData.sunrise - const sunset = sunData.sunset - const showWithin = sunriseSettings.showWithin - const closeToSunrise = closeTo(sunrise) <= showWithin - const closeToSunset = closeTo(sunset) <= showWithin - - // If we only show sometimes and we're not close, return. - if (showWithin > 0 && !closeToSunrise && !closeToSunset) { return } - - // Otherwise, determine which time to show. - const showSunrise = closeTo(sunrise) <= closeTo(sunset) - - // Set up the stack. - const sunriseStack = align(column) - sunriseStack.setPadding(padding/2, padding, padding/2, padding) - sunriseStack.layoutHorizontally() - sunriseStack.centerAlignContent() - - sunriseStack.addSpacer(padding * 0.3) - - // Add the correct symbol. - const symbolName = showSunrise ? "sunrise.fill" : "sunset.fill" - const symbol = sunriseStack.addImage(SFSymbol.named(symbolName).image) - symbol.imageSize = new Size(22,22) - - sunriseStack.addSpacer(padding) - - // Add the time. - const timeText = formatTime(showSunrise ? new Date(sunrise) : new Date(sunset)) - const time = provideText(timeText, sunriseStack, textFormat.sunrise) -} - -// Allow for either term to be used. -async function sunset(column) { - return await sunrise(column) -} - -/* - * HELPER FUNCTIONS - * These functions perform duties for other functions. - * =================================================== - */ - -// Determines if the provided date is at night. -function isNight(dateInput) { - const timeValue = dateInput.getTime() - return (timeValue < sunData.sunrise) || (timeValue > sunData.sunset) -} - -// Determines if two dates occur on the same day -function sameDay(d1, d2) { - return d1.getFullYear() === d2.getFullYear() && - d1.getMonth() === d2.getMonth() && - d1.getDate() === d2.getDate() -} - -// Returns the number of minutes between now and the provided date. -function closeTo(time) { - return Math.abs(currentDate.getTime() - time) / 60000 -} - -// Format the time for a Date input. -function formatTime(date) { - let df = new DateFormatter() - df.locale = locale - df.useNoDateStyle() - df.useShortTimeStyle() - return df.string(date) -} - -// Provide a text symbol with the specified shape. -function provideTextSymbol(shape) { - - // Rectangle character. - if (shape.startsWith("rect")) { - return "\u2759" - } - // Circle character. - if (shape == "circle") { - return "\u2B24" - } - // Default to the rectangle. - return "\u2759" -} - -// Provide a battery SFSymbol with accurate level drawn on top of it. -function provideBatteryIcon() { - - // If we're charging, show the charging icon. - if (Device.isCharging()) { return SFSymbol.named("battery.100.bolt").image } - - // Set the size of the battery icon. - const batteryWidth = 87 - const batteryHeight = 41 - - // Start our draw context. - let draw = new DrawContext() - draw.opaque = false - draw.respectScreenScale = true - draw.size = new Size(batteryWidth, batteryHeight) - - // Draw the battery. - draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight)) - - // Match the battery level values to the SFSymbol. - const x = batteryWidth*0.1525 - const y = batteryHeight*0.247 - const width = batteryWidth*0.602 - const height = batteryHeight*0.505 - - // Prevent unreadable icons. - let level = Device.batteryLevel() - if (level < 0.05) { level = 0.05 } - - // Determine the width and radius of the battery level. - const current = width * level - let radius = height/6.5 - - // When it gets low, adjust the radius to match. - if (current < (radius * 2)) { radius = current / 2 } - - // Make the path for the battery level. - let barPath = new Path() - barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius) - draw.addPath(barPath) - draw.setFillColor(Color.black()) - draw.fillPath() - return draw.getImage() -} - -// Provide a symbol based on the condition. -function provideConditionSymbol(cond,night) { - - // Define our symbol equivalencies. - let symbols = { - - // Thunderstorm - "2": function() { return "cloud.bolt.rain.fill" }, - - // Drizzle - "3": function() { return "cloud.drizzle.fill" }, - - // Rain - "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" }, - - // Snow - "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" }, - - // Atmosphere - "7": function() { - if (cond == 781) { return "tornado" } - if (cond == 701 || cond == 741) { return "cloud.fog.fill" } - return night ? "cloud.fog.fill" : "sun.haze.fill" - }, - - // Clear and clouds - "8": function() { - if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" } - if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" } - return "cloud.fill" - } - } - - // Find out the first digit. - let conditionDigit = Math.floor(cond / 100) - - // Get the symbol. - return SFSymbol.named(symbols[conditionDigit]()).image -} - -// Provide a font based on the input. -function provideFont(fontName, fontSize) { - const fontGenerator = { - "ultralight": function() { return Font.ultraLightSystemFont(fontSize) }, - "light": function() { return Font.lightSystemFont(fontSize) }, - "regular": function() { return Font.regularSystemFont(fontSize) }, - "medium": function() { return Font.mediumSystemFont(fontSize) }, - "semibold": function() { return Font.semiboldSystemFont(fontSize) }, - "bold": function() { return Font.boldSystemFont(fontSize) }, - "heavy": function() { return Font.heavySystemFont(fontSize) }, - "black": function() { return Font.blackSystemFont(fontSize) }, - "italic": function() { return Font.italicSystemFont(fontSize) } - } - - const systemFont = fontGenerator[fontName] - if (systemFont) { return systemFont() } - return new Font(fontName, fontSize) -} - -// Add formatted text to a container. -function provideText(string, container, format) { - const textItem = container.addText(string) - const textFont = format.font || textFormat.defaultText.font - const textSize = format.size || textFormat.defaultText.size - const textColor = format.color || textFormat.defaultText.color - - textItem.font = provideFont(textFont, textSize) - textItem.textColor = new Color(textColor) - return textItem -} - -/* - * DRAWING FUNCTIONS - * These functions draw onto a canvas. - * =================================== - */ - -// Draw the vertical line in the tomorrow view. -function drawVerticalLine(color, height) { - - const width = 2 - - let draw = new DrawContext() - draw.opaque = false - draw.respectScreenScale = true - draw.size = new Size(width,height) - - let barPath = new Path() - const barHeight = height - barPath.addRoundedRect(new Rect(0, 0, width, height), width/2, width/2) - draw.addPath(barPath) - draw.setFillColor(color) - draw.fillPath() - - return draw.getImage() -} - -// Draw the temp bar. -function drawTempBar() { - - // Set the size of the temp bar. - const tempBarWidth = 200 - const tempBarHeight = 20 - - // Calculate the current percentage of the high-low range. - let percent = (weatherData.currentTemp - weatherData.todayLow) / (weatherData.todayHigh - weatherData.todayLow) - - // If we're out of bounds, clip it. - if (percent < 0) { - percent = 0 - } else if (percent > 1) { - percent = 1 - } - - // Determine the scaled x-value for the current temp. - const currPosition = (tempBarWidth - tempBarHeight) * percent - - // Start our draw context. - let draw = new DrawContext() - draw.opaque = false - draw.respectScreenScale = true - draw.size = new Size(tempBarWidth, tempBarHeight) - - // Make the path for the bar. - let barPath = new Path() - const barHeight = tempBarHeight - 10 - barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2) - draw.addPath(barPath) - draw.setFillColor(new Color("ffffff", 0.5)) - draw.fillPath() - - // Make the path for the current temp indicator. - let currPath = new Path() - currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight)) - draw.addPath(currPath) - draw.setFillColor(new Color("ffffff", 1)) - draw.fillPath() - - return draw.getImage() -} From edfc5c32001514a0b5594125b70f789140d454c8 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 07:59:05 +0300 Subject: [PATCH 04/21] Create westher-cal.js --- scripts/westher-cal.js | 1408 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1408 insertions(+) create mode 100644 scripts/westher-cal.js diff --git a/scripts/westher-cal.js b/scripts/westher-cal.js new file mode 100644 index 0000000..77224db --- /dev/null +++ b/scripts/westher-cal.js @@ -0,0 +1,1408 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: teal; icon-glyph: magic; +/* + * SETUP + * Use this section to set up the widget. + * ====================================== + */ + +// To use weather, get a free API key at openweathermap.org/appid and paste it in between the quotation marks. +const apiKey = "" + +// Set the locale code. Leave blank "" to match the device's locale. You can change the hard-coded text strings in the TEXT section below. +let locale = "en" + +// Set to true for fixed location, false to update location as you move around +const lockLocation = true + +// The size of the widget preview in the app. +const widgetPreview = "large" + +// Set to true for an image background, false for no image. +const imageBackground = true + +// Set to true to reset the widget's background image. +const forceImageUpdate = false + +// Set the padding around each item. Default is 5. +const padding = 5 + +/* + * LAYOUT + * Decide what items to show on the widget. + * ======================================== + */ + +// You always need to start with "row," and "column," items, but you can now add as many as you want. +// Adding left, right, or center will align everything after that. The default alignment is left. + +// You can add a flexible vertical space with "space," or a fixed-size space like this: "space(50)" +// Align items to the top or bottom of columns by adding "space," before or after all items in the column. + +// There are many possible items, including: date, greeting, events, current, future, battery, sunrise, and text("Your text here") +// Make sure to always put a comma after each item. + +const items = [ + + row, + + column, + date, + battery, + sunrise, + space, + + column(90), + current, + future, + + row, + + column, + events, + +] + +/* + * ITEM SETTINGS + * Choose how each item is displayed. + * ================================== + */ + +// DATE +// ==== +const dateSettings = { + + // If set to true, date will become smaller when events are displayed. + dynamicDateSize: false + + // If the date is not dynamic, should it be large or small? + ,staticDateSize: "large" + + // Determine the date format for each date type. See docs.scriptable.app/dateformatter + ,smallDateFormat: "EEEE, MMMM d" + ,largeDateLineOne: "EEEE," + ,largeDateLineTwo: "MMMM d" +} + +// EVENTS +// ====== +const eventSettings = { + + // How many events to show. + numberOfEvents: 3 + + // Show all-day events. + ,showAllDay: true + + // Show tomorrow's events. + ,showTomorrow: true + + // Can be blank "" or set to "duration" or "time" to display how long an event is. + ,showEventLength: "duration" + + // Set which calendars for which to show events. Empty [] means all calendars. + ,selectCalendars: [] + + // Leave blank "" for no color, or specify shape (circle, rectangle) and/or side (left, right). + ,showCalendarColor: "rectangle left" + + // When no events remain, show a hard-coded "message", a "greeting", or "none". + ,noEventBehavior: "message" +} + +// SUNRISE +// ======= +const sunriseSettings = { + + // How many minutes before/after sunrise or sunset to show this element. 0 for always. + showWithin: 0 +} + +// WEATHER +// ======= +const weatherSettings = { + + // Set to imperial for Fahrenheit, or metric for Celsius + units: "metric" + + // Show the location of the current weather. + ,showLocation: false + + // Show the text description of the current conditions. + ,showCondition: false + + // Show today's high and low temperatures. + ,showHighLow: true + + // Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it. + ,tomorrowShownAtHour: 20 +} + +/* + * TEXT + * Change the language and formatting of text displayed. + * ===================================================== + */ + +// You can change the language or wording of any text in the widget. +const localizedText = { + + // The text shown if you add a greeting item to the layout. + nightGreeting: "Good night." + ,morningGreeting: "Good morning." + ,afternoonGreeting: "Good afternoon." + ,eveningGreeting: "Good evening." + + // The text shown if you add a future weather item to the layout, or tomorrow's events. + ,nextHourLabel: "Next hour" + ,tomorrowLabel: "Tomorrow" + + // Shown when noEventBehavior is set to "message". + ,noEventMessage: "Enjoy the rest of your day." + + // The text shown after the hours and minutes of an event duration. + ,durationMinute: "m" + ,durationHour: "h" + +} + +// Set the font, size, and color of various text elements. Use iosfonts.com to find fonts to use. If you want to use the default iOS font, set the font name to one of the following: ultralight, light, regular, medium, semibold, bold, heavy, black, or italic. +const textFormat = { + + // Set the default font and color. + defaultText: { size: 14, color: "ffffff", font: "regular" }, + + // Any blank values will use the default. + smallDate: { size: 17, color: "", font: "semibold" }, + largeDate1: { size: 30, color: "", font: "light" }, + largeDate2: { size: 30, color: "", font: "light" }, + + greeting: { size: 30, color: "", font: "semibold" }, + eventLabel: { size: 14, color: "", font: "semibold" }, + eventTitle: { size: 14, color: "", font: "semibold" }, + eventTime: { size: 14, color: "ffffffcc", font: "" }, + noEvents: { size: 30, color: "", font: "semibold" }, + + largeTemp: { size: 34, color: "", font: "light" }, + smallTemp: { size: 14, color: "", font: "" }, + tinyTemp: { size: 12, color: "", font: "" }, + + customText: { size: 14, color: "", font: "" }, + + battery: { size: 14, color: "", font: "medium" }, + sunrise: { size: 14, color: "", font: "medium" }, +} + +/* + * WIDGET CODE + * Be more careful editing this section. + * ===================================== + */ + +// Make sure we have a locale value. +if (locale == "" || locale == null) { locale = Device.locale() } + +// Declare the data variables. +var eventData, locationData, sunData, weatherData + +// Create global constants. +const currentDate = new Date() +const files = FileManager.local() + +/* + * CONSTRUCTION + * ============ + */ + +// Set up the widget with padding. +const widget = new ListWidget() +const horizontalPad = padding < 10 ? 10 - padding : 10 +const verticalPad = padding < 15 ? 15 - padding : 15 +widget.setPadding(horizontalPad, verticalPad, horizontalPad, verticalPad) +widget.spacing = 0 + +// Set up the global variables. +var currentRow = {} +var currentColumn = {} + +// Set up the initial alignment. +var currentAlignment = alignLeft + +// Set up the global ASCII variables. +var currentColumns = [] +var rowNeedsSetup = false + +// It's ASCII time! +if (typeof items[0] == 'string') { + for (line of items[0].split(/\r?\n/)) { await processLine(line) } +} +// Otherwise, set up normally. +else { + for (item of items) { await item(currentColumn) } +} + +/* + * BACKGROUND DISPLAY + * ================== + */ + +// If it's an image background, display it. +if (imageBackground) { + + // Determine if our image exists and when it was saved. + const path = files.joinPath(files.documentsDirectory(), "weather-cal-image") + const exists = files.fileExists(path) + + // If it exists and an update isn't forced, use the cache. + if (exists && (config.runsInWidget || !forceImageUpdate)) { + widget.backgroundImage = files.readImage(path) + + // If it's missing when running in the widget, use a gray background. + } else if (!exists && config.runsInWidget) { + widget.backgroundColor = Color.gray() + + // But if we're running in app, prompt the user for the image. + } else { + const img = await Photos.fromLibrary() + widget.backgroundImage = img + files.writeImage(path, img) + } + +// If it's not an image background, show the gradient. +} else { + let gradient = new LinearGradient() + let gradientSettings = await setupGradient() + + gradient.colors = gradientSettings.color() + gradient.locations = gradientSettings.position() + + widget.backgroundGradient = gradient +} + +// Finish the widget and show a preview. +Script.setWidget(widget) +if (widgetPreview == "small") { widget.presentSmall() } +else if (widgetPreview == "medium") { widget.presentMedium() } +else if (widgetPreview == "large") { widget.presentLarge() } +Script.complete() + +/* + * ASCII FUNCTIONS + * Now isn't this a lot of fun? + * ============================ + */ + +// Provide the named function. +function provideFunction(name) { + const functions = { + space() { return space }, + left() { return left }, + right() { return right }, + center() { return center }, + date() { return date }, + greeting() { return greeting }, + events() { return events }, + current() { return current }, + future() { return future }, + battery() { return battery }, + sunrise() { return sunrise }, + } + return functions[name] +} + +// Processes a single line of ASCII. +async function processLine(lineInput) { + + // Because iOS loves adding periods to everything. + const line = lineInput.replace(/\.+/g,'') + + // If it's blank, return. + if (line.trim() == '') { return } + + // If it's a line, enumerate previous columns (if any) and set up the new row. + if (line[0] == '-' && line[line.length-1] == '-') { + if (currentColumns.length > 0) { await enumerateColumns() } + rowNeedsSetup = true + return + } + + // If it's the first content row, finish the row setup. + if (rowNeedsSetup) { + row(currentColumn) + rowNeedsSetup = false + } + + // If there's a number, this is a setup row. + const setupRow = line.match(/\d+/) + + // Otherwise, it has columns. + const items = line.split('|') + + // Iterate through each item. + for (var i=1; i < items.length-1; i++) { + + // If the current column doesn't exist, make it. + if (!currentColumns[i]) { currentColumns[i] = { items: [] } } + + // Now we have a column to add the items to. + const column = currentColumns[i].items + + // Get the current item and its trimmed version. + const item = items[i] + const trim = item.trim() + + // If it's not a function, figure out spacing. + if (!provideFunction(trim)) { + + // If it's a setup row, whether or not we find the number, we keep going. + if (setupRow) { + const value = parseInt(trim, 10) + if (value) { currentColumns[i].width = value } + continue + } + + // If it's blank and we haven't already added a space, add one. + const prevItem = column[column.length-1] + if (trim == '' && (!prevItem || (prevItem && !prevItem.startsWith("space")))) { + column.push("space") + } + + // Either way, we're done. + continue + + } + + // Determine the alignment. + const index = item.indexOf(trim) + const length = item.slice(index,item.length).length + + let align + if (index > 0 && length > trim.length) { align = "center" } + else if (index > 0) { align = "right" } + else { align = "left" } + + // Add the items to the column. + column.push(align) + column.push(trim) + } +} + +// Runs the function names in each column. +async function enumerateColumns() { + if (currentColumns.length > 0) { + for (col of currentColumns) { + + // If it's null, go to the next one. + if (!col) { continue } + + // If there's a width, use the width function. + if (col.width) { + column(col.width)(currentColumn) + + // Otherwise, create the column normally. + } else { + column(currentColumn) + } + for (item of col.items) { + const func = provideFunction(item)() + await func(currentColumn) + } + } + currentColumns = [] + } +} + +/* + * LAYOUT FUNCTIONS + * These functions manage spacing and alignment. + * ============================================= + */ + +// Makes a new row on the widget. +function row(input = null) { + + function makeRow() { + currentRow = widget.addStack() + currentRow.layoutHorizontally() + currentRow.setPadding(0, 0, 0, 0) + currentColumn.spacing = 0 + + // If input was given, make a column of that size. + if (input > 0) { currentRow.size = new Size(0,input) } + } + + // If there's no input or it's a number, it's being called in the layout declaration. + if (!input || typeof input == "number") { return makeRow } + + // Otherwise, it's being called in the generator. + else { makeRow() } +} + +// Makes a new column on the widget. +function column(input = null) { + + function makeColumn() { + currentColumn = currentRow.addStack() + currentColumn.layoutVertically() + currentColumn.setPadding(0, 0, 0, 0) + currentColumn.spacing = 0 + + // If input was given, make a column of that size. + if (input > 0) { currentColumn.size = new Size(input,0) } + } + + // If there's no input or it's a number, it's being called in the layout declaration. + if (!input || typeof input == "number") { return makeColumn } + + // Otherwise, it's being called in the generator. + else { makeColumn() } +} + +// Create an aligned stack to add content to. +function align(column) { + + // Add the containing stack to the column. + let alignmentStack = column.addStack() + alignmentStack.layoutHorizontally() + + // Get the correct stack from the alignment function. + let returnStack = currentAlignment(alignmentStack) + returnStack.layoutVertically() + return returnStack +} + +// Create a right-aligned stack. +function alignRight(alignmentStack) { + alignmentStack.addSpacer() + let returnStack = alignmentStack.addStack() + return returnStack +} + +// Create a left-aligned stack. +function alignLeft(alignmentStack) { + let returnStack = alignmentStack.addStack() + alignmentStack.addSpacer() + return returnStack +} + +// Create a center-aligned stack. +function alignCenter(alignmentStack) { + alignmentStack.addSpacer() + let returnStack = alignmentStack.addStack() + alignmentStack.addSpacer() + return returnStack +} + +// This function adds a space, with an optional amount. +function space(input = null) { + + // This function adds a spacer with the input width. + function spacer(column) { + + // If the input is null or zero, add a flexible spacer. + if (!input || input == 0) { column.addSpacer() } + + // Otherwise, add a space with the specified length. + else { column.addSpacer(input) } + } + + // If there's no input or it's a number, it's being called in the column declaration. + if (!input || typeof input == "number") { return spacer } + + // Otherwise, it's being called in the column generator. + else { input.addSpacer() } +} + +// Change the current alignment to right. +function right(x) { currentAlignment = alignRight } + +// Change the current alignment to left. +function left(x) { currentAlignment = alignLeft } + +// Change the current alignment to center. +function center(x) { currentAlignment = alignCenter } + +/* + * SETUP FUNCTIONS + * These functions prepare data needed for items. + * ============================================== + */ + +// Set up the eventData object. +async function setupEvents() { + + eventData = {} + const calendars = eventSettings.selectCalendars + const numberOfEvents = eventSettings.numberOfEvents + + // Function to determine if an event should be shown. + function shouldShowEvent(event) { + + // If events are filtered and the calendar isn't in the selected calendars, return false. + if (calendars.length && !calendars.includes(event.calendar.title)) { return false } + + // Hack to remove canceled Office 365 events. + if (event.title.startsWith("Canceled:")) { return false } + + // If it's an all-day event, only show if the setting is active. + if (event.isAllDay) { return eventSettings.showAllDay } + + // Otherwise, return the event if it's in the future. + return (event.startDate.getTime() > currentDate.getTime()) + } + + // Determine which events to show, and how many. + const todayEvents = await CalendarEvent.today([]) + let shownEvents = 0 + let futureEvents = [] + + for (const event of todayEvents) { + if (shownEvents == numberOfEvents) { break } + if (shouldShowEvent(event)) { + futureEvents.push(event) + shownEvents++ + } + } + + // If there's room and we need to, show tomorrow's events. + let multipleTomorrowEvents = false + if (eventSettings.showTomorrow && shownEvents < numberOfEvents) { + + const tomorrowEvents = await CalendarEvent.tomorrow([]) + for (const event of tomorrowEvents) { + if (shownEvents == numberOfEvents) { break } + if (shouldShowEvent(event)) { + + // Add the tomorrow label prior to the first tomorrow event. + if (!multipleTomorrowEvents) { + + // The tomorrow label is pretending to be an event. + futureEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isLabel: true }) + multipleTomorrowEvents = true + } + + // Show the tomorrow event and increment the counter. + futureEvents.push(event) + shownEvents++ + } + } + } + + // Store the future events, and whether or not any events are displayed. + eventData.futureEvents = futureEvents + eventData.eventsAreVisible = (futureEvents.length > 0) && (eventSettings.numberOfEvents > 0) +} + +// Set up the gradient for the widget background. +async function setupGradient() { + + // Requirements: sunrise + if (!sunData) { await setupSunrise() } + + let gradient = { + dawn: { + color() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] }, + position() { return [0, 0.5, 1] }, + }, + + sunrise: { + color() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] }, + position() { return [0, 0.8, 1.5] }, + }, + + midday: { + color() { return [new Color("3a8cc1"), new Color("90c0df")] }, + position() { return [0, 1] }, + }, + + noon: { + color() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] }, + position() { return [-0.2, 0.2, 1.5] }, + }, + + sunset: { + color() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] }, + position() { return [0.1, 0.9, 1.2] }, + }, + + twilight: { + color() { return [new Color("021033"), new Color("16296b"), new Color("414791")] }, + position() { return [0, 0.5, 1] }, + }, + + night: { + color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] }, + position() { return [-0.5, 0.2, 0.5, 1] }, + }, + } + + const sunrise = sunData.sunrise + const sunset = sunData.sunset + + // Use sunrise or sunset if we're within 30min of it. + if (closeTo(sunrise)<=15) { return gradient.sunrise } + if (closeTo(sunset)<=15) { return gradient.sunset } + + // In the 30min before/after, use dawn/twilight. + if (closeTo(sunrise)<=45 && utcTime < sunrise) { return gradient.dawn } + if (closeTo(sunset)<=45 && utcTime > sunset) { return gradient.twilight } + + // Otherwise, if it's night, return night. + if (isNight(currentDate)) { return gradient.night } + + // If it's around noon, the sun is high in the sky. + if (currentDate.getHours() == 12) { return gradient.noon } + + // Otherwise, return the "typical" theme. + return gradient.midday +} + +// Set up the locationData object. +async function setupLocation() { + + locationData = {} + const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc") + + // If our location is unlocked or cache doesn't exist, ask iOS for location. + var readLocationFromFile = false + if (!lockLocation || !files.fileExists(locationPath)) { + try { + const location = await Location.current() + const geocode = await Location.reverseGeocode(location.latitude, location.longitude, locale) + locationData.latitude = location.latitude + locationData.longitude = location.longitude + locationData.locality = geocode[0].locality + files.writeString(locationPath, location.latitude + "|" + location.longitude + "|" + locationData.locality) + + } catch(e) { + // If we fail in unlocked mode, read it from the cache. + if (!lockLocation) { readLocationFromFile = true } + + // We can't recover if we fail on first run in locked mode. + else { return } + } + } + + // If our location is locked or we need to read from file, do it. + if (lockLocation || readLocationFromFile) { + const locationStr = files.readString(locationPath).split("|") + locationData.latitude = locationStr[0] + locationData.longitude = locationStr[1] + locationData.locality = locationStr[2] + } +} + +// Set up the sunData object. +async function setupSunrise() { + + // Requirements: location + if (!locationData) { await setupLocation() } + + // Set up the sunrise/sunset cache. + const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sun") + const sunCacheExists = files.fileExists(sunCachePath) + const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0 + let sunDataRaw, afterSunset + + // If cache exists and was created today, use cached data. + if (sunCacheExists && sameDay(currentDate, sunCacheDate)) { + const sunCache = files.readString(sunCachePath) + sunDataRaw = JSON.parse(sunCache) + + // Determine if it's after sunset. + const sunsetDate = new Date(sunDataRaw.results.sunset) + afterSunset = currentDate.getTime() - sunsetDate.getTime() > (45 * 60 * 1000) + } + + // If we don't have data yet, or we need to get tomorrow's data, get it from the server. + if (!sunDataRaw || afterSunset) { + let tomorrowDate = new Date() + tomorrowDate.setDate(currentDate.getDate() + 1) + const dateToUse = afterSunset ? tomorrowDate : currentDate + const sunReq = "https://api.sunrise-sunset.org/json?lat=" + locationData.latitude + "&lng=" + locationData.longitude + "&formatted=0&date=" + dateToUse.getFullYear() + "-" + (dateToUse.getMonth()+1) + "-" + dateToUse.getDate() + sunDataRaw = await new Request(sunReq).loadJSON() + files.writeString(sunCachePath, JSON.stringify(sunDataRaw)) + } + + // Store the timing values. + sunData = {} + sunData.sunrise = new Date(sunDataRaw.results.sunrise).getTime() + sunData.sunset = new Date(sunDataRaw.results.sunset).getTime() +} + +// Set up the weatherData object. +async function setupWeather() { + + // Requirements: location + if (!locationData) { await setupLocation() } + + // Set up the cache. + const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache") + const cacheExists = files.fileExists(cachePath) + const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0 + var weatherDataRaw + + // If cache exists and it's been less than 60 seconds since last request, use cached data. + if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) { + const cache = files.readString(cachePath) + weatherDataRaw = JSON.parse(cache) + + // Otherwise, use the API to get new weather data. + } else { + const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + locationData.latitude + "&lon=" + locationData.longitude + "&exclude=minutely,alerts&units=" + weatherSettings.units + "&lang=" + locale + "&appid=" + apiKey + weatherDataRaw = await new Request(weatherReq).loadJSON() + files.writeString(cachePath, JSON.stringify(weatherDataRaw)) + } + + // Store the weather values. + weatherData = {} + weatherData.currentTemp = weatherDataRaw.current.temp + weatherData.currentCondition = weatherDataRaw.current.weather[0].id + weatherData.currentDescription = weatherDataRaw.current.weather[0].main + weatherData.todayHigh = weatherDataRaw.daily[0].temp.max + weatherData.todayLow = weatherDataRaw.daily[0].temp.min + + weatherData.nextHourTemp = weatherDataRaw.hourly[1].temp + weatherData.nextHourCondition = weatherDataRaw.hourly[1].weather[0].id + + weatherData.tomorrowHigh = weatherDataRaw.daily[1].temp.max + weatherData.tomorrowLow = weatherDataRaw.daily[1].temp.min + weatherData.tomorrowCondition = weatherDataRaw.daily[1].weather[0].id +} + +/* + * WIDGET ITEMS + * These functions display items on the widget. + * ============================================ + */ + +// Display the date on the widget. +async function date(column) { + + // Requirements: events (if dynamicDateSize is enabled) + if (!eventData && dateSettings.dynamicDateSize) { await setupEvents() } + + // Set up the date formatter and set its locale. + let df = new DateFormatter() + df.locale = locale + + // Show small if it's hard coded, or if it's dynamic and events are visible. + if (dateSettings.staticDateSize == "small" || (dateSettings.dynamicDateSize && eventData.eventsAreVisible)) { + let dateStack = align(column) + dateStack.setPadding(padding, padding, padding, padding) + + df.dateFormat = dateSettings.smallDateFormat + let dateText = provideText(df.string(currentDate), dateStack, textFormat.smallDate) + + // Otherwise, show the large date. + } else { + let dateOneStack = align(column) + df.dateFormat = dateSettings.largeDateLineOne + let dateOne = provideText(df.string(currentDate), dateOneStack, textFormat.largeDate1) + dateOneStack.setPadding(padding/2, padding, 0, padding) + + let dateTwoStack = align(column) + df.dateFormat = dateSettings.largeDateLineTwo + let dateTwo = provideText(df.string(currentDate), dateTwoStack, textFormat.largeDate2) + dateTwoStack.setPadding(0, padding, padding, padding) + } +} + +// Display a time-based greeting on the widget. +async function greeting(column) { + + // This function makes a greeting based on the time of day. + function makeGreeting() { + const hour = currentDate.getHours() + if (hour < 5) { return localizedText.nightGreeting } + if (hour < 12) { return localizedText.morningGreeting } + if (hour-12 < 5) { return localizedText.afternoonGreeting } + if (hour-12 < 10) { return localizedText.eveningGreeting } + return localizedText.nightGreeting + } + + // Set up the greeting. + let greetingStack = align(column) + let greeting = provideText(makeGreeting(), greetingStack, textFormat.greeting) + greetingStack.setPadding(padding, padding, padding, padding) +} + +// Display events on the widget. +async function events(column) { + + // Requirements: events + if (!eventData) { await setupEvents() } + + // If no events are visible, figure out what to do. + if (!eventData.eventsAreVisible) { + const display = eventSettings.noEventBehavior + + // If it's a greeting, let the greeting function handle it. + if (display == "greeting") { return await greeting(column) } + + // If it's a message, get the localized text. + if (display == "message" && localizedText.noEventMessage.length) { + const messageStack = align(column) + messageStack.setPadding(padding, padding, padding, padding) + provideText(localizedText.noEventMessage, messageStack, textFormat.noEvents) + } + + // Whether or not we displayed something, return here. + return + } + + // Set up the event stack. + let eventStack = column.addStack() + eventStack.layoutVertically() + const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200 + eventStack.url = 'calshow:' + todaySeconds + + // If there are no events and we have a message, show it and return. + if (!eventData.eventsAreVisible && localizedText.noEventMessage.length) { + let message = provideText(localizedText.noEventMessage, eventStack, textFormat.noEvents) + eventStack.setPadding(padding, padding, padding, padding) + return + } + + // If we're not showing the message, don't pad the event stack. + eventStack.setPadding(0, 0, 0, 0) + + // Add each event to the stack. + var currentStack = eventStack + const futureEvents = eventData.futureEvents + for (let i = 0; i < futureEvents.length; i++) { + + const event = futureEvents[i] + const bottomPadding = (padding-10 < 0) ? 0 : padding-10 + + // If it's the tomorrow label, change to the tomorrow stack. + if (event.isLabel) { + let tomorrowStack = column.addStack() + tomorrowStack.layoutVertically() + const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800 + tomorrowStack.url = 'calshow:' + tomorrowSeconds + currentStack = tomorrowStack + + // Mimic the formatting of an event title, mostly. + const eventLabelStack = align(currentStack) + const eventLabel = provideText(event.title, eventLabelStack, textFormat.eventLabel) + eventLabelStack.setPadding(padding, padding, padding, padding) + continue + } + + const titleStack = align(currentStack) + titleStack.layoutHorizontally() + const showCalendarColor = eventSettings.showCalendarColor + const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle" + + // If we're showing a color, and it's not shown on the right, add it to the left. + if (showCalendarColor.length && !showCalendarColor.includes("right")) { + let colorItemText = provideTextSymbol(colorShape) + " " + let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) + colorItem.textColor = event.calendar.color + } + + const title = provideText(event.title.trim(), titleStack, textFormat.eventTitle) + titleStack.setPadding(padding, padding, event.isAllDay ? padding : padding/5, padding) + + // If we're showing a color on the right, show it. + if (showCalendarColor.length && showCalendarColor.includes("right")) { + let colorItemText = " " + provideTextSymbol(colorShape) + let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) + colorItem.textColor = event.calendar.color + } + + // If there are too many events, limit the line height. + if (futureEvents.length >= 3) { title.lineLimit = 1 } + + // If it's an all-day event, we don't need a time. + if (event.isAllDay) { continue } + + // Format the time information. + let timeText = formatTime(event.startDate) + + // If we show the length as time, add an en dash and the time. + if (eventSettings.showEventLength == "time") { + timeText += "–" + formatTime(event.endDate) + + // If we should it as a duration, add the minutes. + } else if (eventSettings.showEventLength == "duration") { + const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60) + const hours = Math.floor(duration/60) + const minutes = Math.floor(duration % 60) + const hourText = hours>0 ? hours + localizedText.durationHour : "" + const minuteText = minutes>0 ? minutes + localizedText.durationMinute : "" + const showSpace = hourText.length && minuteText.length + timeText += " \u2022 " + hourText + (showSpace ? " " : "") + minuteText + } + + const timeStack = align(currentStack) + const time = provideText(timeText, timeStack, textFormat.eventTime) + timeStack.setPadding(0, padding, padding, padding) + } +} + +// Display the current weather. +async function current(column) { + + // Requirements: weather and sunrise + if (!weatherData) { await setupWeather() } + if (!sunData) { await setupSunrise() } + + // Set up the current weather stack. + let currentWeatherStack = column.addStack() + currentWeatherStack.layoutVertically() + currentWeatherStack.setPadding(0, 0, 0, 0) + currentWeatherStack.url = "https://weather.com/weather/today/l/" + locationData.latitude + "," + locationData.longitude + + // If we're showing the location, add it. + if (weatherSettings.showLocation) { + let locationTextStack = align(currentWeatherStack) + let locationText = provideText(locationData.locality, locationTextStack, textFormat.smallTemp) + locationTextStack.setPadding(padding, padding, padding, padding) + } + + // Show the current condition symbol. + let mainConditionStack = align(currentWeatherStack) + let mainCondition = mainConditionStack.addImage(provideConditionSymbol(weatherData.currentCondition,isNight(currentDate))) + mainCondition.imageSize = new Size(22,22) + mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : padding, padding, 0, padding) + + // If we're showing the description, add it. + if (weatherSettings.showCondition) { + let conditionTextStack = align(currentWeatherStack) + let conditionText = provideText(weatherData.currentDescription, conditionTextStack, textFormat.smallTemp) + conditionTextStack.setPadding(padding, padding, 0, padding) + } + + // Show the current temperature. + const tempStack = align(currentWeatherStack) + tempStack.setPadding(0, padding, 0, padding) + const tempText = Math.round(weatherData.currentTemp) + "°" + const temp = provideText(tempText, tempStack, textFormat.largeTemp) + + // If we're not showing the high and low, end it here. + if (!weatherSettings.showHighLow) { return } + + // Show the temp bar and high/low values. + let tempBarStack = align(currentWeatherStack) + tempBarStack.layoutVertically() + tempBarStack.setPadding(0, padding, padding, padding) + + let tempBar = drawTempBar() + let tempBarImage = tempBarStack.addImage(tempBar) + tempBarImage.size = new Size(50,0) + + tempBarStack.addSpacer(1) + + let highLowStack = tempBarStack.addStack() + highLowStack.layoutHorizontally() + + const mainLowText = Math.round(weatherData.todayLow).toString() + const mainLow = provideText(mainLowText, highLowStack, textFormat.tinyTemp) + highLowStack.addSpacer() + const mainHighText = Math.round(weatherData.todayHigh).toString() + const mainHigh = provideText(mainHighText, highLowStack, textFormat.tinyTemp) + + tempBarStack.size = new Size(60,30) +} + +// Display upcoming weather. +async function future(column) { + + // Requirements: weather and sunrise + if (!weatherData) { await setupWeather() } + if (!sunData) { await setupSunrise() } + + // Set up the future weather stack. + let futureWeatherStack = column.addStack() + futureWeatherStack.layoutVertically() + futureWeatherStack.setPadding(0, 0, 0, 0) + futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + locationData.latitude + "," + locationData.longitude + + // Determine if we should show the next hour. + const showNextHour = (currentDate.getHours() < weatherSettings.tomorrowShownAtHour) + + // Set the label value. + const subLabelStack = align(futureWeatherStack) + const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel + const subLabel = provideText(subLabelText, subLabelStack, textFormat.smallTemp) + subLabelStack.setPadding(0, padding, padding/2, padding) + + // Set up the sub condition stack. + let subConditionStack = align(futureWeatherStack) + subConditionStack.layoutHorizontally() + subConditionStack.centerAlignContent() + subConditionStack.setPadding(0, padding, padding, padding) + + // Determine if it will be night in the next hour. + var nightCondition + if (showNextHour) { + const addHour = currentDate.getTime() + (60*60*1000) + const newDate = new Date(addHour) + nightCondition = isNight(newDate) + } else { + nightCondition = false + } + + let subCondition = subConditionStack.addImage(provideConditionSymbol(showNextHour ? weatherData.nextHourCondition : weatherData.tomorrowCondition,nightCondition)) + const subConditionSize = showNextHour ? 14 : 18 + subCondition.imageSize = new Size(subConditionSize, subConditionSize) + subConditionStack.addSpacer(5) + + // The next part of the display changes significantly for next hour vs tomorrow. + if (showNextHour) { + const subTempText = Math.round(weatherData.nextHourTemp) + "°" + const subTemp = provideText(subTempText, subConditionStack, textFormat.smallTemp) + + } else { + let tomorrowLine = subConditionStack.addImage(drawVerticalLine(new Color("ffffff", 0.5), 20)) + tomorrowLine.imageSize = new Size(3,28) + subConditionStack.addSpacer(5) + let tomorrowStack = subConditionStack.addStack() + tomorrowStack.layoutVertically() + + const tomorrowHighText = Math.round(weatherData.tomorrowHigh) + "" + const tomorrowHigh = provideText(tomorrowHighText, tomorrowStack, textFormat.tinyTemp) + tomorrowStack.addSpacer(4) + const tomorrowLowText = Math.round(weatherData.tomorrowLow) + "" + const tomorrowLow = provideText(tomorrowLowText, tomorrowStack, textFormat.tinyTemp) + } +} + +// Return a text-creation function. +function text(input = null) { + + function displayText(column) { + + // Don't do anything if the input is blank. + if (!input || input == "") { return } + + // Otherwise, add the text. + const textStack = align(column) + textStack.setPadding(padding, padding, padding, padding) + const textDisplay = provideText(input, textStack, textFormat.customText) + } + return displayText +} + +// Add a battery element to the widget; consisting of a battery icon and percentage. +async function battery(column) { + + // Get battery level via Scriptable function and format it in a convenient way + function getBatteryLevel() { + + const batteryLevel = Device.batteryLevel() + const batteryPercentage = `${Math.round(batteryLevel * 100)}%` + + return batteryPercentage + } + + const batteryLevel = Device.batteryLevel() + + // Set up the battery level item + let batteryStack = align(column) + batteryStack.layoutHorizontally() + batteryStack.centerAlignContent() + + let batteryIcon = batteryStack.addImage(provideBatteryIcon()) + batteryIcon.imageSize = new Size(30,30) + + // Change the battery icon to red if battery level is <= 20 to match system behavior + if ( Math.round(batteryLevel * 100) > 20 || Device.isCharging() ) { + + batteryIcon.tintColor = new Color(textFormat.battery.color || textFormat.defaultText.color) + + } else { + + batteryIcon.tintColor = Color.red() + + } + + batteryStack.addSpacer(padding * 0.6) + + // Display the battery status + let batteryInfo = provideText(getBatteryLevel(), batteryStack, textFormat.battery) + + batteryStack.setPadding(padding/2, padding, padding/2, padding) + +} + +// Show the sunrise or sunset time. +async function sunrise(column) { + + // Requirements: sunrise + if (!sunData) { await setupSunrise() } + + const sunrise = sunData.sunrise + const sunset = sunData.sunset + const showWithin = sunriseSettings.showWithin + const closeToSunrise = closeTo(sunrise) <= showWithin + const closeToSunset = closeTo(sunset) <= showWithin + + // If we only show sometimes and we're not close, return. + if (showWithin > 0 && !closeToSunrise && !closeToSunset) { return } + + // Otherwise, determine which time to show. + const showSunrise = closeTo(sunrise) <= closeTo(sunset) + + // Set up the stack. + const sunriseStack = align(column) + sunriseStack.setPadding(padding/2, padding, padding/2, padding) + sunriseStack.layoutHorizontally() + sunriseStack.centerAlignContent() + + sunriseStack.addSpacer(padding * 0.3) + + // Add the correct symbol. + const symbolName = showSunrise ? "sunrise.fill" : "sunset.fill" + const symbol = sunriseStack.addImage(SFSymbol.named(symbolName).image) + symbol.imageSize = new Size(22,22) + + sunriseStack.addSpacer(padding) + + // Add the time. + const timeText = formatTime(showSunrise ? new Date(sunrise) : new Date(sunset)) + const time = provideText(timeText, sunriseStack, textFormat.sunrise) +} + +// Allow for either term to be used. +async function sunset(column) { + return await sunrise(column) +} + +/* + * HELPER FUNCTIONS + * These functions perform duties for other functions. + * =================================================== + */ + +// Determines if the provided date is at night. +function isNight(dateInput) { + const timeValue = dateInput.getTime() + return (timeValue < sunData.sunrise) || (timeValue > sunData.sunset) +} + +// Determines if two dates occur on the same day +function sameDay(d1, d2) { + return d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() +} + +// Returns the number of minutes between now and the provided date. +function closeTo(time) { + return Math.abs(currentDate.getTime() - time) / 60000 +} + +// Format the time for a Date input. +function formatTime(date) { + let df = new DateFormatter() + df.locale = locale + df.useNoDateStyle() + df.useShortTimeStyle() + return df.string(date) +} + +// Provide a text symbol with the specified shape. +function provideTextSymbol(shape) { + + // Rectangle character. + if (shape.startsWith("rect")) { + return "\u2759" + } + // Circle character. + if (shape == "circle") { + return "\u2B24" + } + // Default to the rectangle. + return "\u2759" +} + +// Provide a battery SFSymbol with accurate level drawn on top of it. +function provideBatteryIcon() { + + // If we're charging, show the charging icon. + if (Device.isCharging()) { return SFSymbol.named("battery.100.bolt").image } + + // Set the size of the battery icon. + const batteryWidth = 87 + const batteryHeight = 41 + + // Start our draw context. + let draw = new DrawContext() + draw.opaque = false + draw.respectScreenScale = true + draw.size = new Size(batteryWidth, batteryHeight) + + // Draw the battery. + draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight)) + + // Match the battery level values to the SFSymbol. + const x = batteryWidth*0.1525 + const y = batteryHeight*0.247 + const width = batteryWidth*0.602 + const height = batteryHeight*0.505 + + // Prevent unreadable icons. + let level = Device.batteryLevel() + if (level < 0.05) { level = 0.05 } + + // Determine the width and radius of the battery level. + const current = width * level + let radius = height/6.5 + + // When it gets low, adjust the radius to match. + if (current < (radius * 2)) { radius = current / 2 } + + // Make the path for the battery level. + let barPath = new Path() + barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius) + draw.addPath(barPath) + draw.setFillColor(Color.black()) + draw.fillPath() + return draw.getImage() +} + +// Provide a symbol based on the condition. +function provideConditionSymbol(cond,night) { + + // Define our symbol equivalencies. + let symbols = { + + // Thunderstorm + "2": function() { return "cloud.bolt.rain.fill" }, + + // Drizzle + "3": function() { return "cloud.drizzle.fill" }, + + // Rain + "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" }, + + // Snow + "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" }, + + // Atmosphere + "7": function() { + if (cond == 781) { return "tornado" } + if (cond == 701 || cond == 741) { return "cloud.fog.fill" } + return night ? "cloud.fog.fill" : "sun.haze.fill" + }, + + // Clear and clouds + "8": function() { + if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" } + if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" } + return "cloud.fill" + } + } + + // Find out the first digit. + let conditionDigit = Math.floor(cond / 100) + + // Get the symbol. + return SFSymbol.named(symbols[conditionDigit]()).image +} + +// Provide a font based on the input. +function provideFont(fontName, fontSize) { + const fontGenerator = { + "ultralight": function() { return Font.ultraLightSystemFont(fontSize) }, + "light": function() { return Font.lightSystemFont(fontSize) }, + "regular": function() { return Font.regularSystemFont(fontSize) }, + "medium": function() { return Font.mediumSystemFont(fontSize) }, + "semibold": function() { return Font.semiboldSystemFont(fontSize) }, + "bold": function() { return Font.boldSystemFont(fontSize) }, + "heavy": function() { return Font.heavySystemFont(fontSize) }, + "black": function() { return Font.blackSystemFont(fontSize) }, + "italic": function() { return Font.italicSystemFont(fontSize) } + } + + const systemFont = fontGenerator[fontName] + if (systemFont) { return systemFont() } + return new Font(fontName, fontSize) +} + +// Add formatted text to a container. +function provideText(string, container, format) { + const textItem = container.addText(string) + const textFont = format.font || textFormat.defaultText.font + const textSize = format.size || textFormat.defaultText.size + const textColor = format.color || textFormat.defaultText.color + + textItem.font = provideFont(textFont, textSize) + textItem.textColor = new Color(textColor) + return textItem +} + +/* + * DRAWING FUNCTIONS + * These functions draw onto a canvas. + * =================================== + */ + +// Draw the vertical line in the tomorrow view. +function drawVerticalLine(color, height) { + + const width = 2 + + let draw = new DrawContext() + draw.opaque = false + draw.respectScreenScale = true + draw.size = new Size(width,height) + + let barPath = new Path() + const barHeight = height + barPath.addRoundedRect(new Rect(0, 0, width, height), width/2, width/2) + draw.addPath(barPath) + draw.setFillColor(color) + draw.fillPath() + + return draw.getImage() +} + +// Draw the temp bar. +function drawTempBar() { + + // Set the size of the temp bar. + const tempBarWidth = 200 + const tempBarHeight = 20 + + // Calculate the current percentage of the high-low range. + let percent = (weatherData.currentTemp - weatherData.todayLow) / (weatherData.todayHigh - weatherData.todayLow) + + // If we're out of bounds, clip it. + if (percent < 0) { + percent = 0 + } else if (percent > 1) { + percent = 1 + } + + // Determine the scaled x-value for the current temp. + const currPosition = (tempBarWidth - tempBarHeight) * percent + + // Start our draw context. + let draw = new DrawContext() + draw.opaque = false + draw.respectScreenScale = true + draw.size = new Size(tempBarWidth, tempBarHeight) + + // Make the path for the bar. + let barPath = new Path() + const barHeight = tempBarHeight - 10 + barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2) + draw.addPath(barPath) + draw.setFillColor(new Color("ffffff", 0.5)) + draw.fillPath() + + // Make the path for the current temp indicator. + let currPath = new Path() + currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight)) + draw.addPath(currPath) + draw.setFillColor(new Color("ffffff", 1)) + draw.fillPath() + + return draw.getImage() +} From 07dfdc0744a967e12eb3e31ab15230099b370cce Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:00:08 +0300 Subject: [PATCH 05/21] Create WallabagUnread.js --- scripts/WallabagUnread.js | 104 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 scripts/WallabagUnread.js diff --git a/scripts/WallabagUnread.js b/scripts/WallabagUnread.js new file mode 100644 index 0000000..e1476d3 --- /dev/null +++ b/scripts/WallabagUnread.js @@ -0,0 +1,104 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: light-gray; icon-glyph: book; + +/** + * WIDGET CONFIGURATION + */ +const LIGHT_BG_COLOUR = '#F1F1F1' +const DARK_BG_COLOUR = '#F1F1F1' +const INSTAPAPER_RSS_FEED_URL = "" + +const data = await fetchData() +const widget = await createWidget(data) + +// Check if the script is running in +// a widget. If not, show a preview of +// the widget to easier debug it. +if (!config.runsInWidget) { + await widget.presentSmall() +} +// Tell the system to show the widget. +Script.setWidget(widget) +Script.complete() + +async function createWidget(data) { + const gradientBg = [ + new Color(`${LIGHT_BG_COLOUR}D9`), + new Color(`${DARK_BG_COLOUR}D9`), + ] + const gradient = new LinearGradient() + gradient.locations = [0, 1] + gradient.colors = gradientBg + const bg = new Color(LIGHT_BG_COLOUR) + const logoReq = await new Request('https://i.imgur.com/xiKYzQP.png') + const logoImg = await logoReq.loadImage() + + const w = new ListWidget() + w.useDefaultPadding() + w.backgroundColor = bg + w.backgroundGradient = gradient + + const itemFontSize = config.widgetFamily === 'large' ? 15 : 12 + + const headerRow = w.addStack() + headerRow.layoutHorizontally() + + const wimg = headerRow.addImage(logoImg) + wimg.imageSize = new Size(18, 18) + headerRow.addSpacer(10) + + const title = + config.widgetFamily === 'small' ? 'Instapaper' : 'Wallabag: Unread' + const headerTitle = headerRow.addText(title) + headerTitle.font = Font.semiboldSystemFont(15) + headerTitle.textColor = Color.black() + headerTitle.textOpacity = 0.9 + + w.addSpacer(10) + + const widgetCount = config.widgetFamily === 'large' ? 7 : 3 + data.forEach(({ title, link }, index) => { + if (index > widgetCount) { + return + } + const itemTitle = w.addText(title[0]) + itemTitle.font = Font.systemFont(itemFontSize) + itemTitle.textColor = Color.black() + itemTitle.textOpacity = 0.9 + itemTitle.url = link[0] + + if (index < widgetCount) { + const spacing = config.widgetFamily === 'large' ? 8 : 6 + w.addSpacer(spacing) + } + }) + + return w +} + +async function fetch(url) { + const req = new Request(url) + const json = await req.loadJSON() + return json +} + +async function fetchData() { + const unreadData = await fetch( + `https://rsstojson.com/v1/api/?rss_url=${INSTAPAPER_RSS_FEED_URL}` + ) + + return unreadData.rss.channel[0].item +} + +function addSymbol({ + symbol = 'applelogo', + stack, + color = Color.black(), + size = 20, +}) { + const _sym = SFSymbol.named(symbol) + const wImg = stack.addImage(_sym.image) + wImg.tintColor = color + wImg.imageSize = new Size(size, size) +} From 8264bb6f7a4912af242e7078a6a1f48177e74534 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:01:45 +0300 Subject: [PATCH 06/21] Create Vultr Bandwidth .js --- scripts/Vultr Bandwidth .js | 74 +++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 scripts/Vultr Bandwidth .js diff --git a/scripts/Vultr Bandwidth .js b/scripts/Vultr Bandwidth .js new file mode 100644 index 0000000..6a98e7f --- /dev/null +++ b/scripts/Vultr Bandwidth .js @@ -0,0 +1,74 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: deep-gray; icon-glyph: magic; + +let widget = await createWidget(); +Script.setWidget(widget); + + +async function createWidget() { + const url = "https://api.vultr.com/v1/server/list" + let req = new Request(url); + req.method = 'GET'; + let APIKEY = ""; + req.headers = { + "API-Key": APIKEY, + "Content-Type": "application/json", + "Accept": "application/json" + }; + const data = await req.loadJSON(); + let widgetHeader = "vultr" + let CLOUD = data[0].current_bandwidth_gb; + let VPN = data[0].current_bandwidth_gb; + let YTDL = data[0].current_bandwidth_gb; + console.log(CLOUD) + console.log(VPN) + console.log(YTDL) + let w = new ListWidget(); + let titleTxt = w.addText(widgetHeader); + titleTxt.centerAlignText(16); + let bodyTxt0 = w.addText("vpn " + VPN + " GB"); + let bodyTxt1 = w.addText("Cloud " + CLOUD + " GB"); + let bodyTxt2 = w.addText("YTDL " + YTDL + " GB"); + // w.backgroundColor = new Color("#1b1c1f"); +const files = FileManager.local(); +const forceImageUpdate = true; +const imageBackground = true; + +if (imageBackground) { + + // Determine if our image exists and when it was saved. + const path = files.joinPath(files.documentsDirectory(), "IPsWidget-cache") + const exists = files.fileExists(path) + + // If it exists and an update isn't forced, use the cache. + if (exists && (config.runsInWidget || !forceImageUpdate)) { + w.backgroundImage = files.readImage(path) + + // If it's missing when running in the widget, use a gray background. + } else if (!exists && config.runsInWidget) { + w.backgroundColor = Color.gray() + + // But if we're running in app, prompt the user for the image. + } else { + const img = await Photos.fromLibrary() + w.backgroundImage = img + files.writeImage(path, img) + }; + + // If it's not an image background, show the gradient. + } else { + let gradient = new LinearGradient() + let gradientSettings = await setupGradient() + + gradient.colors = gradientSettings.color() + gradient.locations = gradientSettings.position() + + w.backgroundGradient = gradient + }; + + // Finish the widget and show a preview. + Script.setWidget(w); + + return w; +}; From cdeac2bca529b4d80034016e9de38a47855f7831 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:04:24 +0300 Subject: [PATCH 07/21] Create IPsWidget.js --- scripts/IPsWidget.js | 85 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 scripts/IPsWidget.js diff --git a/scripts/IPsWidget.js b/scripts/IPsWidget.js new file mode 100644 index 0000000..3ef9b5b --- /dev/null +++ b/scripts/IPsWidget.js @@ -0,0 +1,85 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: deep-gray; icon-glyph: wifi; +let widget = await createWidget(); +Script.setWidget(widget); + +async function createWidget() { + + +// CloudFlare +const url = "" +let req = new Request(url); +req.method = 'GET'; +let authHeader = "" +let authHeader2 = "" +req.headers = { + "X-Auth-Email": authHeader2, + "X-Auth-Key": authHeader, + "Content-Type": "application/json", + "Accept": "application/json" +}; +const data = await req.loadJSON(); +let CFIP = data.result.content; + +// IPINFO.IO +const reqq = new Request("https://ipinfo.io/json"); +const dataa = await reqq.loadJSON(); +let IPNOW = dataa.ip + if (IPNOW == "") { + IP = "VPN ON" + } else { + IP = dataa.ip + }; + +// Widget Constraction +let w = new ListWidget(); +let WidgetHeader = "IPCenter"; +let titleTxt = w.addText(WidgetHeader); +titleTxt.centerAlignText(20); +let bodyTxt = w.addText("CloudFlare: " + CFIP); +let bodyTxtt = w.addText("MyIP: " + IP) +// w.backgroundColor = new Color("#1b1c1f"); +const files = FileManager.local() + +const forceImageUpdate = false +const imageBackground = true + +if (imageBackground) { + + // Determine if our image exists and when it was saved. + const path = files.joinPath(files.documentsDirectory(), "IPsWidget-cache") + const exists = files.fileExists(path) + + // If it exists and an update isn't forced, use the cache. + if (exists && (config.runsInWidget || !forceImageUpdate)) { + w.backgroundImage = files.readImage(path) + + // If it's missing when running in the widget, use a gray background. + } else if (!exists && config.runsInWidget) { + w.backgroundColor = Color.gray() + + // But if we're running in app, prompt the user for the image. + } else { + const img = await Photos.fromLibrary() + w.backgroundImage = img + files.writeImage(path, img) + } + + // If it's not an image background, show the gradient. + } else { + let gradient = new LinearGradient() + let gradientSettings = await setupGradient() + + gradient.colors = gradientSettings.color() + gradient.locations = gradientSettings.position() + + w.backgroundGradient = gradient + } + + // Finish the widget and show a preview. + Script.setWidget(w) + +return w; + +}; From 1edbd807803272a7b11e826bc647b372f9e052ad Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:05:39 +0300 Subject: [PATCH 08/21] Create InstapaperUnread.js --- scripts/InstapaperUnread.js | 104 ++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 scripts/InstapaperUnread.js diff --git a/scripts/InstapaperUnread.js b/scripts/InstapaperUnread.js new file mode 100644 index 0000000..0039435 --- /dev/null +++ b/scripts/InstapaperUnread.js @@ -0,0 +1,104 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: light-gray; icon-glyph: book; + +/** + * WIDGET CONFIGURATION + */ +const LIGHT_BG_COLOUR = '#F1F1F1' +const DARK_BG_COLOUR = '#F1F1F1' +const INSTAPAPER_RSS_FEED_URL = "" + +const data = await fetchData() +const widget = await createWidget(data) + +// Check if the script is running in +// a widget. If not, show a preview of +// the widget to easier debug it. +if (!config.runsInWidget) { + await widget.presentSmall() +} +// Tell the system to show the widget. +Script.setWidget(widget) +Script.complete() + +async function createWidget(data) { + const gradientBg = [ + new Color(`${LIGHT_BG_COLOUR}D9`), + new Color(`${DARK_BG_COLOUR}D9`), + ] + const gradient = new LinearGradient() + gradient.locations = [0, 1] + gradient.colors = gradientBg + const bg = new Color(LIGHT_BG_COLOUR) + const logoReq = await new Request('https://i.imgur.com/BKjVm7c.png') + const logoImg = await logoReq.loadImage() + + const w = new ListWidget() + w.useDefaultPadding() + w.backgroundColor = bg + w.backgroundGradient = gradient + + const itemFontSize = config.widgetFamily === 'large' ? 15 : 12 + + const headerRow = w.addStack() + headerRow.layoutHorizontally() + + const wimg = headerRow.addImage(logoImg) + wimg.imageSize = new Size(18, 18) + headerRow.addSpacer(10) + + const title = + config.widgetFamily === 'small' ? 'Instapaper' : 'Instapaper: Unread' + const headerTitle = headerRow.addText(title) + headerTitle.font = Font.semiboldSystemFont(15) + headerTitle.textColor = Color.black() + headerTitle.textOpacity = 0.9 + + w.addSpacer(10) + + const widgetCount = config.widgetFamily === 'large' ? 7 : 3 + data.forEach(({ title, link }, index) => { + if (index > widgetCount) { + return + } + const itemTitle = w.addText(title[0]) + itemTitle.font = Font.systemFont(itemFontSize) + itemTitle.textColor = Color.black() + itemTitle.textOpacity = 0.9 + itemTitle.url = link[0] + + if (index < widgetCount) { + const spacing = config.widgetFamily === 'large' ? 8 : 6 + w.addSpacer(spacing) + } + }) + + return w +} + +async function fetch(url) { + const req = new Request(url) + const json = await req.loadJSON() + return json +} + +async function fetchData() { + const unreadData = await fetch( + `https://rsstojson.com/v1/api/?rss_url=${INSTAPAPER_RSS_FEED_URL}` + ) + + return unreadData.rss.channel[0].item +} + +function addSymbol({ + symbol = 'applelogo', + stack, + color = Color.black(), + size = 20, +}) { + const _sym = SFSymbol.named(symbol) + const wImg = stack.addImage(_sym.image) + wImg.tintColor = color + wImg.imageSize = new Size(size, size) +} From ee8c89d98909fee42f1d6d83707447ebc5a9e365 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:06:27 +0300 Subject: [PATCH 09/21] Create Hello.js --- scripts/Hello.js | 121 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 scripts/Hello.js diff --git a/scripts/Hello.js b/scripts/Hello.js new file mode 100644 index 0000000..a3bfb73 --- /dev/null +++ b/scripts/Hello.js @@ -0,0 +1,121 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: gray; icon-glyph: grin-tongue-wink; +// To use, add a parameter to the widget with a format of: image.png|padding-top|text-color +// The image should be placed in the iCloud Scriptable folder (case-sensitive). +// The padding-top spacing parameter moves the text down by a set amount. +// The text color parameter should be a hex value. + +// For example, to use the image bkg_fall.PNG with a padding of 40 and a text color of red, +// the parameter should be typed as: bkg_fall.png|40|#ff0000 + +// All parameters are required and separated with "|" + +let widgetHello = new ListWidget(); +var today = new Date(); + +var widgetInputRAW = args.widgetParameter; + +try { + widgetInputRAW.toString(); +} catch(e) { + throw new Error("Please long press the widget and add a parameter."); +} + +var widgetInput = widgetInputRAW.toString(); + +var inputArr = widgetInput.split("|"); + +var scriptableFilePath = "/var/mobile/Library/Mobile Documents/iCloud~dk~simonbs~Scriptable/Documents/"; +var removeSpaces1 = inputArr[0].split(" "); +var removeSpaces2 = removeSpaces1.join(''); +var tempPath = removeSpaces2.split("."); +var backgroundImageURLRAW = scriptableFilePath + tempPath[0]; + +var fileTypes = ['png', 'jpg', 'jpeg', 'tiff', 'webp']; + +var fm = FileManager.iCloud(); +var backgroundImageURL = scriptableFilePath + tempPath[0] + "."; + +fileTypes.forEach(function(item) { + if (fm.fileExists((backgroundImageURL + item.toLowerCase())) == true) { + backgroundImageURL = backgroundImageURLRAW + "." + item.toLowerCase(); + } else if (fm.fileExists((backgroundImageURL + item.toUpperCase())) == true) { + backgroundImageURL = backgroundImageURLRAW + "." + item.toUpperCase(); + } +}); + +var spacing = parseInt(inputArr[1]); + + +var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; +var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; + +var greetingsMorning = [ +'Good morning,' +]; +var greetingsAfternoon = [ +'Good afternoon,' +]; +var greetingsEvening = [ +'Good evening,' +]; +var greetingsNight = [ +'Bedtime,' +]; +var greetingsLateNight = [ +'Go to sleep!,' +]; + +var weekday = days[ today.getDay() ]; +var month = months[ today.getMonth() ]; +var date = today.getDate(); +var hour = today.getHours(); + +var datefull = weekday + ", " + month + " " + date; + +function randomGreeting(greetingArray) { + return Math.floor(Math.random() * greetingArray.length); +} + +var greeting = new String("Howdy.") +// var greeting = "Howdy."; +if (hour < 5 && hour >= 1) { + greeting = greetingsLateNight[randomGreeting(greetingsLateNight)]; +} else if (hour >= 23 || hour < 1) { + greeting = greetingsNight[randomGreeting(greetingsNight)]; +} else if (hour < 12) { + greeting = greetingsMorning[randomGreeting(greetingsMorning)]; +} else if (hour >= 12 && hour <= 17) { + greeting = greetingsAfternoon[randomGreeting(greetingsAfternoon)]; +} else if (hour > 17 && hour < 23) { + greeting = greetingsEvening[randomGreeting(greetingsEvening)]; +} + +if (month == "October" && date == 31) { + greeting = "Happy Halloween!"; +} +if (month == "December" && date == 25) { + greeting = "Merry Christmas!"; +} + +try { + inputArr[2].toString(); +} catch(e) { + throw new Error("Please long press the widget and add a parameter."); +} + +let themeColor = new Color(inputArr[2].toString()); + +widgetHello.addSpacer(parseInt(spacing)); +let hello = widgetHello.addText(greeting + " Abdulrahman."); +hello.font = Font.boldSystemFont(23); +hello.textColor = themeColor; +let datetext = widgetHello.addText(datefull); +datetext.font = Font.regularSystemFont(18); +datetext.textColor = themeColor; +widgetHello.addSpacer(); +widgetHello.setPadding(15, 7, 10, 0) +widgetHello.backgroundImage = Image.fromFile(backgroundImageURL); + +Script.setWidget(widgetHello); From 82d714c2179909c699e60ba38a37f123f5d5824d Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:07:31 +0300 Subject: [PATCH 10/21] Create Coronavirus.js --- scripts/Coronavirus.js | 67 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 scripts/Coronavirus.js diff --git a/scripts/Coronavirus.js b/scripts/Coronavirus.js new file mode 100644 index 0000000..da472e1 --- /dev/null +++ b/scripts/Coronavirus.js @@ -0,0 +1,67 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: deep-green; icon-glyph: user-md; +// change "country" to a value from https://coronavirus-19-api.herokuapp.com/countries/ +const country = "" +const url = `https://coronavirus-19-api.herokuapp.com/countries/${country}` +const req = new Request(url) +const res = await req.loadJSON() + +if (config.runsInWidget) { + // create and show widget + let widget = createWidget("Coronavirus", `${res.todayCases} Today`, `${res.cases} Total`, "#000000") + Script.setWidget(widget) + Script.complete() +} else { + // make table + let table = new UITable() + + // add header + let row = new UITableRow() + row.isHeader = true + row.addText(`Coronavirus Stats in Saudi Arabia`) + table.addRow(row) + + // fill data + table.addRow(createRow("Cases", res.cases)) + table.addRow(createRow("Today", res.todayCases)) + table.addRow(createRow("Deaths", res.deaths)) + table.addRow(createRow("Recovered", res.recovered)) + table.addRow(createRow("Critical", res.critical)) + + if (config.runsWithSiri) + Speech.speak(`There are ${res.cases} cases in ${country}, and ${res.todayCases} cases today.`) + + // present table + table.present() +} + +function createRow(title, number) { + let row = new UITableRow() + row.addText(title) + row.addText(number.toString()).rightAligned() + return row +} + +function createWidget(pretitle, title, subtitle, color) { + let w = new ListWidget() +// w.backgroundColor = new Color(color) + let preTxt = w.addText(pretitle) +// preTxt.textColor = Color.white() + preTxt.textOpacity = 0.9 + preTxt.font = Font.systemFont(16) + w.addSpacer(5) + let titleTxt = w.addText(title) +// titleTxt.textColor = Color.white() + titleTxt.font = Font.systemFont(22) + w.addSpacer(5) + let subTxt = w.addText(subtitle) +// subTxt.textColor = Color.white() + subTxt.textOpacity = 0.9 + subTxt.font = Font.systemFont(18) +// let ImgUrl = "https://cdn.id7x.xyz/scriptable/bottom-right.png" +// let mmm = new Request(ImgUrl); +// let bacc = await mmm.loadImage(); +// w.backgroundImage = bacc + return w +} From 686fa6b97aeab10143fcf6aacdfadea046112383 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:07:56 +0300 Subject: [PATCH 11/21] Update Coronavirus.js --- scripts/Coronavirus.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scripts/Coronavirus.js b/scripts/Coronavirus.js index da472e1..75e4666 100644 --- a/scripts/Coronavirus.js +++ b/scripts/Coronavirus.js @@ -59,9 +59,5 @@ function createWidget(pretitle, title, subtitle, color) { // subTxt.textColor = Color.white() subTxt.textOpacity = 0.9 subTxt.font = Font.systemFont(18) -// let ImgUrl = "https://cdn.id7x.xyz/scriptable/bottom-right.png" -// let mmm = new Request(ImgUrl); -// let bacc = await mmm.loadImage(); -// w.backgroundImage = bacc return w } From c044399fd27a4684f5133e115bb5bbb725187949 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Mon, 26 Oct 2020 08:10:14 +0300 Subject: [PATCH 12/21] Update WallabagUnread.js --- scripts/WallabagUnread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/WallabagUnread.js b/scripts/WallabagUnread.js index e1476d3..9f0af39 100644 --- a/scripts/WallabagUnread.js +++ b/scripts/WallabagUnread.js @@ -49,7 +49,7 @@ async function createWidget(data) { headerRow.addSpacer(10) const title = - config.widgetFamily === 'small' ? 'Instapaper' : 'Wallabag: Unread' + config.widgetFamily === 'small' ? 'Wallabag' : 'Wallabag: Unread' const headerTitle = headerRow.addText(title) headerTitle.font = Font.semiboldSystemFont(15) headerTitle.textColor = Color.black() From 458dbe1687c077ed6f3b69078c0b4f33c8496496 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:44:44 +0300 Subject: [PATCH 13/21] Delete westher-cal.js --- scripts/westher-cal.js | 1408 ---------------------------------------- 1 file changed, 1408 deletions(-) delete mode 100644 scripts/westher-cal.js diff --git a/scripts/westher-cal.js b/scripts/westher-cal.js deleted file mode 100644 index 77224db..0000000 --- a/scripts/westher-cal.js +++ /dev/null @@ -1,1408 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: teal; icon-glyph: magic; -/* - * SETUP - * Use this section to set up the widget. - * ====================================== - */ - -// To use weather, get a free API key at openweathermap.org/appid and paste it in between the quotation marks. -const apiKey = "" - -// Set the locale code. Leave blank "" to match the device's locale. You can change the hard-coded text strings in the TEXT section below. -let locale = "en" - -// Set to true for fixed location, false to update location as you move around -const lockLocation = true - -// The size of the widget preview in the app. -const widgetPreview = "large" - -// Set to true for an image background, false for no image. -const imageBackground = true - -// Set to true to reset the widget's background image. -const forceImageUpdate = false - -// Set the padding around each item. Default is 5. -const padding = 5 - -/* - * LAYOUT - * Decide what items to show on the widget. - * ======================================== - */ - -// You always need to start with "row," and "column," items, but you can now add as many as you want. -// Adding left, right, or center will align everything after that. The default alignment is left. - -// You can add a flexible vertical space with "space," or a fixed-size space like this: "space(50)" -// Align items to the top or bottom of columns by adding "space," before or after all items in the column. - -// There are many possible items, including: date, greeting, events, current, future, battery, sunrise, and text("Your text here") -// Make sure to always put a comma after each item. - -const items = [ - - row, - - column, - date, - battery, - sunrise, - space, - - column(90), - current, - future, - - row, - - column, - events, - -] - -/* - * ITEM SETTINGS - * Choose how each item is displayed. - * ================================== - */ - -// DATE -// ==== -const dateSettings = { - - // If set to true, date will become smaller when events are displayed. - dynamicDateSize: false - - // If the date is not dynamic, should it be large or small? - ,staticDateSize: "large" - - // Determine the date format for each date type. See docs.scriptable.app/dateformatter - ,smallDateFormat: "EEEE, MMMM d" - ,largeDateLineOne: "EEEE," - ,largeDateLineTwo: "MMMM d" -} - -// EVENTS -// ====== -const eventSettings = { - - // How many events to show. - numberOfEvents: 3 - - // Show all-day events. - ,showAllDay: true - - // Show tomorrow's events. - ,showTomorrow: true - - // Can be blank "" or set to "duration" or "time" to display how long an event is. - ,showEventLength: "duration" - - // Set which calendars for which to show events. Empty [] means all calendars. - ,selectCalendars: [] - - // Leave blank "" for no color, or specify shape (circle, rectangle) and/or side (left, right). - ,showCalendarColor: "rectangle left" - - // When no events remain, show a hard-coded "message", a "greeting", or "none". - ,noEventBehavior: "message" -} - -// SUNRISE -// ======= -const sunriseSettings = { - - // How many minutes before/after sunrise or sunset to show this element. 0 for always. - showWithin: 0 -} - -// WEATHER -// ======= -const weatherSettings = { - - // Set to imperial for Fahrenheit, or metric for Celsius - units: "metric" - - // Show the location of the current weather. - ,showLocation: false - - // Show the text description of the current conditions. - ,showCondition: false - - // Show today's high and low temperatures. - ,showHighLow: true - - // Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it. - ,tomorrowShownAtHour: 20 -} - -/* - * TEXT - * Change the language and formatting of text displayed. - * ===================================================== - */ - -// You can change the language or wording of any text in the widget. -const localizedText = { - - // The text shown if you add a greeting item to the layout. - nightGreeting: "Good night." - ,morningGreeting: "Good morning." - ,afternoonGreeting: "Good afternoon." - ,eveningGreeting: "Good evening." - - // The text shown if you add a future weather item to the layout, or tomorrow's events. - ,nextHourLabel: "Next hour" - ,tomorrowLabel: "Tomorrow" - - // Shown when noEventBehavior is set to "message". - ,noEventMessage: "Enjoy the rest of your day." - - // The text shown after the hours and minutes of an event duration. - ,durationMinute: "m" - ,durationHour: "h" - -} - -// Set the font, size, and color of various text elements. Use iosfonts.com to find fonts to use. If you want to use the default iOS font, set the font name to one of the following: ultralight, light, regular, medium, semibold, bold, heavy, black, or italic. -const textFormat = { - - // Set the default font and color. - defaultText: { size: 14, color: "ffffff", font: "regular" }, - - // Any blank values will use the default. - smallDate: { size: 17, color: "", font: "semibold" }, - largeDate1: { size: 30, color: "", font: "light" }, - largeDate2: { size: 30, color: "", font: "light" }, - - greeting: { size: 30, color: "", font: "semibold" }, - eventLabel: { size: 14, color: "", font: "semibold" }, - eventTitle: { size: 14, color: "", font: "semibold" }, - eventTime: { size: 14, color: "ffffffcc", font: "" }, - noEvents: { size: 30, color: "", font: "semibold" }, - - largeTemp: { size: 34, color: "", font: "light" }, - smallTemp: { size: 14, color: "", font: "" }, - tinyTemp: { size: 12, color: "", font: "" }, - - customText: { size: 14, color: "", font: "" }, - - battery: { size: 14, color: "", font: "medium" }, - sunrise: { size: 14, color: "", font: "medium" }, -} - -/* - * WIDGET CODE - * Be more careful editing this section. - * ===================================== - */ - -// Make sure we have a locale value. -if (locale == "" || locale == null) { locale = Device.locale() } - -// Declare the data variables. -var eventData, locationData, sunData, weatherData - -// Create global constants. -const currentDate = new Date() -const files = FileManager.local() - -/* - * CONSTRUCTION - * ============ - */ - -// Set up the widget with padding. -const widget = new ListWidget() -const horizontalPad = padding < 10 ? 10 - padding : 10 -const verticalPad = padding < 15 ? 15 - padding : 15 -widget.setPadding(horizontalPad, verticalPad, horizontalPad, verticalPad) -widget.spacing = 0 - -// Set up the global variables. -var currentRow = {} -var currentColumn = {} - -// Set up the initial alignment. -var currentAlignment = alignLeft - -// Set up the global ASCII variables. -var currentColumns = [] -var rowNeedsSetup = false - -// It's ASCII time! -if (typeof items[0] == 'string') { - for (line of items[0].split(/\r?\n/)) { await processLine(line) } -} -// Otherwise, set up normally. -else { - for (item of items) { await item(currentColumn) } -} - -/* - * BACKGROUND DISPLAY - * ================== - */ - -// If it's an image background, display it. -if (imageBackground) { - - // Determine if our image exists and when it was saved. - const path = files.joinPath(files.documentsDirectory(), "weather-cal-image") - const exists = files.fileExists(path) - - // If it exists and an update isn't forced, use the cache. - if (exists && (config.runsInWidget || !forceImageUpdate)) { - widget.backgroundImage = files.readImage(path) - - // If it's missing when running in the widget, use a gray background. - } else if (!exists && config.runsInWidget) { - widget.backgroundColor = Color.gray() - - // But if we're running in app, prompt the user for the image. - } else { - const img = await Photos.fromLibrary() - widget.backgroundImage = img - files.writeImage(path, img) - } - -// If it's not an image background, show the gradient. -} else { - let gradient = new LinearGradient() - let gradientSettings = await setupGradient() - - gradient.colors = gradientSettings.color() - gradient.locations = gradientSettings.position() - - widget.backgroundGradient = gradient -} - -// Finish the widget and show a preview. -Script.setWidget(widget) -if (widgetPreview == "small") { widget.presentSmall() } -else if (widgetPreview == "medium") { widget.presentMedium() } -else if (widgetPreview == "large") { widget.presentLarge() } -Script.complete() - -/* - * ASCII FUNCTIONS - * Now isn't this a lot of fun? - * ============================ - */ - -// Provide the named function. -function provideFunction(name) { - const functions = { - space() { return space }, - left() { return left }, - right() { return right }, - center() { return center }, - date() { return date }, - greeting() { return greeting }, - events() { return events }, - current() { return current }, - future() { return future }, - battery() { return battery }, - sunrise() { return sunrise }, - } - return functions[name] -} - -// Processes a single line of ASCII. -async function processLine(lineInput) { - - // Because iOS loves adding periods to everything. - const line = lineInput.replace(/\.+/g,'') - - // If it's blank, return. - if (line.trim() == '') { return } - - // If it's a line, enumerate previous columns (if any) and set up the new row. - if (line[0] == '-' && line[line.length-1] == '-') { - if (currentColumns.length > 0) { await enumerateColumns() } - rowNeedsSetup = true - return - } - - // If it's the first content row, finish the row setup. - if (rowNeedsSetup) { - row(currentColumn) - rowNeedsSetup = false - } - - // If there's a number, this is a setup row. - const setupRow = line.match(/\d+/) - - // Otherwise, it has columns. - const items = line.split('|') - - // Iterate through each item. - for (var i=1; i < items.length-1; i++) { - - // If the current column doesn't exist, make it. - if (!currentColumns[i]) { currentColumns[i] = { items: [] } } - - // Now we have a column to add the items to. - const column = currentColumns[i].items - - // Get the current item and its trimmed version. - const item = items[i] - const trim = item.trim() - - // If it's not a function, figure out spacing. - if (!provideFunction(trim)) { - - // If it's a setup row, whether or not we find the number, we keep going. - if (setupRow) { - const value = parseInt(trim, 10) - if (value) { currentColumns[i].width = value } - continue - } - - // If it's blank and we haven't already added a space, add one. - const prevItem = column[column.length-1] - if (trim == '' && (!prevItem || (prevItem && !prevItem.startsWith("space")))) { - column.push("space") - } - - // Either way, we're done. - continue - - } - - // Determine the alignment. - const index = item.indexOf(trim) - const length = item.slice(index,item.length).length - - let align - if (index > 0 && length > trim.length) { align = "center" } - else if (index > 0) { align = "right" } - else { align = "left" } - - // Add the items to the column. - column.push(align) - column.push(trim) - } -} - -// Runs the function names in each column. -async function enumerateColumns() { - if (currentColumns.length > 0) { - for (col of currentColumns) { - - // If it's null, go to the next one. - if (!col) { continue } - - // If there's a width, use the width function. - if (col.width) { - column(col.width)(currentColumn) - - // Otherwise, create the column normally. - } else { - column(currentColumn) - } - for (item of col.items) { - const func = provideFunction(item)() - await func(currentColumn) - } - } - currentColumns = [] - } -} - -/* - * LAYOUT FUNCTIONS - * These functions manage spacing and alignment. - * ============================================= - */ - -// Makes a new row on the widget. -function row(input = null) { - - function makeRow() { - currentRow = widget.addStack() - currentRow.layoutHorizontally() - currentRow.setPadding(0, 0, 0, 0) - currentColumn.spacing = 0 - - // If input was given, make a column of that size. - if (input > 0) { currentRow.size = new Size(0,input) } - } - - // If there's no input or it's a number, it's being called in the layout declaration. - if (!input || typeof input == "number") { return makeRow } - - // Otherwise, it's being called in the generator. - else { makeRow() } -} - -// Makes a new column on the widget. -function column(input = null) { - - function makeColumn() { - currentColumn = currentRow.addStack() - currentColumn.layoutVertically() - currentColumn.setPadding(0, 0, 0, 0) - currentColumn.spacing = 0 - - // If input was given, make a column of that size. - if (input > 0) { currentColumn.size = new Size(input,0) } - } - - // If there's no input or it's a number, it's being called in the layout declaration. - if (!input || typeof input == "number") { return makeColumn } - - // Otherwise, it's being called in the generator. - else { makeColumn() } -} - -// Create an aligned stack to add content to. -function align(column) { - - // Add the containing stack to the column. - let alignmentStack = column.addStack() - alignmentStack.layoutHorizontally() - - // Get the correct stack from the alignment function. - let returnStack = currentAlignment(alignmentStack) - returnStack.layoutVertically() - return returnStack -} - -// Create a right-aligned stack. -function alignRight(alignmentStack) { - alignmentStack.addSpacer() - let returnStack = alignmentStack.addStack() - return returnStack -} - -// Create a left-aligned stack. -function alignLeft(alignmentStack) { - let returnStack = alignmentStack.addStack() - alignmentStack.addSpacer() - return returnStack -} - -// Create a center-aligned stack. -function alignCenter(alignmentStack) { - alignmentStack.addSpacer() - let returnStack = alignmentStack.addStack() - alignmentStack.addSpacer() - return returnStack -} - -// This function adds a space, with an optional amount. -function space(input = null) { - - // This function adds a spacer with the input width. - function spacer(column) { - - // If the input is null or zero, add a flexible spacer. - if (!input || input == 0) { column.addSpacer() } - - // Otherwise, add a space with the specified length. - else { column.addSpacer(input) } - } - - // If there's no input or it's a number, it's being called in the column declaration. - if (!input || typeof input == "number") { return spacer } - - // Otherwise, it's being called in the column generator. - else { input.addSpacer() } -} - -// Change the current alignment to right. -function right(x) { currentAlignment = alignRight } - -// Change the current alignment to left. -function left(x) { currentAlignment = alignLeft } - -// Change the current alignment to center. -function center(x) { currentAlignment = alignCenter } - -/* - * SETUP FUNCTIONS - * These functions prepare data needed for items. - * ============================================== - */ - -// Set up the eventData object. -async function setupEvents() { - - eventData = {} - const calendars = eventSettings.selectCalendars - const numberOfEvents = eventSettings.numberOfEvents - - // Function to determine if an event should be shown. - function shouldShowEvent(event) { - - // If events are filtered and the calendar isn't in the selected calendars, return false. - if (calendars.length && !calendars.includes(event.calendar.title)) { return false } - - // Hack to remove canceled Office 365 events. - if (event.title.startsWith("Canceled:")) { return false } - - // If it's an all-day event, only show if the setting is active. - if (event.isAllDay) { return eventSettings.showAllDay } - - // Otherwise, return the event if it's in the future. - return (event.startDate.getTime() > currentDate.getTime()) - } - - // Determine which events to show, and how many. - const todayEvents = await CalendarEvent.today([]) - let shownEvents = 0 - let futureEvents = [] - - for (const event of todayEvents) { - if (shownEvents == numberOfEvents) { break } - if (shouldShowEvent(event)) { - futureEvents.push(event) - shownEvents++ - } - } - - // If there's room and we need to, show tomorrow's events. - let multipleTomorrowEvents = false - if (eventSettings.showTomorrow && shownEvents < numberOfEvents) { - - const tomorrowEvents = await CalendarEvent.tomorrow([]) - for (const event of tomorrowEvents) { - if (shownEvents == numberOfEvents) { break } - if (shouldShowEvent(event)) { - - // Add the tomorrow label prior to the first tomorrow event. - if (!multipleTomorrowEvents) { - - // The tomorrow label is pretending to be an event. - futureEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isLabel: true }) - multipleTomorrowEvents = true - } - - // Show the tomorrow event and increment the counter. - futureEvents.push(event) - shownEvents++ - } - } - } - - // Store the future events, and whether or not any events are displayed. - eventData.futureEvents = futureEvents - eventData.eventsAreVisible = (futureEvents.length > 0) && (eventSettings.numberOfEvents > 0) -} - -// Set up the gradient for the widget background. -async function setupGradient() { - - // Requirements: sunrise - if (!sunData) { await setupSunrise() } - - let gradient = { - dawn: { - color() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] }, - position() { return [0, 0.5, 1] }, - }, - - sunrise: { - color() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] }, - position() { return [0, 0.8, 1.5] }, - }, - - midday: { - color() { return [new Color("3a8cc1"), new Color("90c0df")] }, - position() { return [0, 1] }, - }, - - noon: { - color() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] }, - position() { return [-0.2, 0.2, 1.5] }, - }, - - sunset: { - color() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] }, - position() { return [0.1, 0.9, 1.2] }, - }, - - twilight: { - color() { return [new Color("021033"), new Color("16296b"), new Color("414791")] }, - position() { return [0, 0.5, 1] }, - }, - - night: { - color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] }, - position() { return [-0.5, 0.2, 0.5, 1] }, - }, - } - - const sunrise = sunData.sunrise - const sunset = sunData.sunset - - // Use sunrise or sunset if we're within 30min of it. - if (closeTo(sunrise)<=15) { return gradient.sunrise } - if (closeTo(sunset)<=15) { return gradient.sunset } - - // In the 30min before/after, use dawn/twilight. - if (closeTo(sunrise)<=45 && utcTime < sunrise) { return gradient.dawn } - if (closeTo(sunset)<=45 && utcTime > sunset) { return gradient.twilight } - - // Otherwise, if it's night, return night. - if (isNight(currentDate)) { return gradient.night } - - // If it's around noon, the sun is high in the sky. - if (currentDate.getHours() == 12) { return gradient.noon } - - // Otherwise, return the "typical" theme. - return gradient.midday -} - -// Set up the locationData object. -async function setupLocation() { - - locationData = {} - const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc") - - // If our location is unlocked or cache doesn't exist, ask iOS for location. - var readLocationFromFile = false - if (!lockLocation || !files.fileExists(locationPath)) { - try { - const location = await Location.current() - const geocode = await Location.reverseGeocode(location.latitude, location.longitude, locale) - locationData.latitude = location.latitude - locationData.longitude = location.longitude - locationData.locality = geocode[0].locality - files.writeString(locationPath, location.latitude + "|" + location.longitude + "|" + locationData.locality) - - } catch(e) { - // If we fail in unlocked mode, read it from the cache. - if (!lockLocation) { readLocationFromFile = true } - - // We can't recover if we fail on first run in locked mode. - else { return } - } - } - - // If our location is locked or we need to read from file, do it. - if (lockLocation || readLocationFromFile) { - const locationStr = files.readString(locationPath).split("|") - locationData.latitude = locationStr[0] - locationData.longitude = locationStr[1] - locationData.locality = locationStr[2] - } -} - -// Set up the sunData object. -async function setupSunrise() { - - // Requirements: location - if (!locationData) { await setupLocation() } - - // Set up the sunrise/sunset cache. - const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sun") - const sunCacheExists = files.fileExists(sunCachePath) - const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0 - let sunDataRaw, afterSunset - - // If cache exists and was created today, use cached data. - if (sunCacheExists && sameDay(currentDate, sunCacheDate)) { - const sunCache = files.readString(sunCachePath) - sunDataRaw = JSON.parse(sunCache) - - // Determine if it's after sunset. - const sunsetDate = new Date(sunDataRaw.results.sunset) - afterSunset = currentDate.getTime() - sunsetDate.getTime() > (45 * 60 * 1000) - } - - // If we don't have data yet, or we need to get tomorrow's data, get it from the server. - if (!sunDataRaw || afterSunset) { - let tomorrowDate = new Date() - tomorrowDate.setDate(currentDate.getDate() + 1) - const dateToUse = afterSunset ? tomorrowDate : currentDate - const sunReq = "https://api.sunrise-sunset.org/json?lat=" + locationData.latitude + "&lng=" + locationData.longitude + "&formatted=0&date=" + dateToUse.getFullYear() + "-" + (dateToUse.getMonth()+1) + "-" + dateToUse.getDate() - sunDataRaw = await new Request(sunReq).loadJSON() - files.writeString(sunCachePath, JSON.stringify(sunDataRaw)) - } - - // Store the timing values. - sunData = {} - sunData.sunrise = new Date(sunDataRaw.results.sunrise).getTime() - sunData.sunset = new Date(sunDataRaw.results.sunset).getTime() -} - -// Set up the weatherData object. -async function setupWeather() { - - // Requirements: location - if (!locationData) { await setupLocation() } - - // Set up the cache. - const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache") - const cacheExists = files.fileExists(cachePath) - const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0 - var weatherDataRaw - - // If cache exists and it's been less than 60 seconds since last request, use cached data. - if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) { - const cache = files.readString(cachePath) - weatherDataRaw = JSON.parse(cache) - - // Otherwise, use the API to get new weather data. - } else { - const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + locationData.latitude + "&lon=" + locationData.longitude + "&exclude=minutely,alerts&units=" + weatherSettings.units + "&lang=" + locale + "&appid=" + apiKey - weatherDataRaw = await new Request(weatherReq).loadJSON() - files.writeString(cachePath, JSON.stringify(weatherDataRaw)) - } - - // Store the weather values. - weatherData = {} - weatherData.currentTemp = weatherDataRaw.current.temp - weatherData.currentCondition = weatherDataRaw.current.weather[0].id - weatherData.currentDescription = weatherDataRaw.current.weather[0].main - weatherData.todayHigh = weatherDataRaw.daily[0].temp.max - weatherData.todayLow = weatherDataRaw.daily[0].temp.min - - weatherData.nextHourTemp = weatherDataRaw.hourly[1].temp - weatherData.nextHourCondition = weatherDataRaw.hourly[1].weather[0].id - - weatherData.tomorrowHigh = weatherDataRaw.daily[1].temp.max - weatherData.tomorrowLow = weatherDataRaw.daily[1].temp.min - weatherData.tomorrowCondition = weatherDataRaw.daily[1].weather[0].id -} - -/* - * WIDGET ITEMS - * These functions display items on the widget. - * ============================================ - */ - -// Display the date on the widget. -async function date(column) { - - // Requirements: events (if dynamicDateSize is enabled) - if (!eventData && dateSettings.dynamicDateSize) { await setupEvents() } - - // Set up the date formatter and set its locale. - let df = new DateFormatter() - df.locale = locale - - // Show small if it's hard coded, or if it's dynamic and events are visible. - if (dateSettings.staticDateSize == "small" || (dateSettings.dynamicDateSize && eventData.eventsAreVisible)) { - let dateStack = align(column) - dateStack.setPadding(padding, padding, padding, padding) - - df.dateFormat = dateSettings.smallDateFormat - let dateText = provideText(df.string(currentDate), dateStack, textFormat.smallDate) - - // Otherwise, show the large date. - } else { - let dateOneStack = align(column) - df.dateFormat = dateSettings.largeDateLineOne - let dateOne = provideText(df.string(currentDate), dateOneStack, textFormat.largeDate1) - dateOneStack.setPadding(padding/2, padding, 0, padding) - - let dateTwoStack = align(column) - df.dateFormat = dateSettings.largeDateLineTwo - let dateTwo = provideText(df.string(currentDate), dateTwoStack, textFormat.largeDate2) - dateTwoStack.setPadding(0, padding, padding, padding) - } -} - -// Display a time-based greeting on the widget. -async function greeting(column) { - - // This function makes a greeting based on the time of day. - function makeGreeting() { - const hour = currentDate.getHours() - if (hour < 5) { return localizedText.nightGreeting } - if (hour < 12) { return localizedText.morningGreeting } - if (hour-12 < 5) { return localizedText.afternoonGreeting } - if (hour-12 < 10) { return localizedText.eveningGreeting } - return localizedText.nightGreeting - } - - // Set up the greeting. - let greetingStack = align(column) - let greeting = provideText(makeGreeting(), greetingStack, textFormat.greeting) - greetingStack.setPadding(padding, padding, padding, padding) -} - -// Display events on the widget. -async function events(column) { - - // Requirements: events - if (!eventData) { await setupEvents() } - - // If no events are visible, figure out what to do. - if (!eventData.eventsAreVisible) { - const display = eventSettings.noEventBehavior - - // If it's a greeting, let the greeting function handle it. - if (display == "greeting") { return await greeting(column) } - - // If it's a message, get the localized text. - if (display == "message" && localizedText.noEventMessage.length) { - const messageStack = align(column) - messageStack.setPadding(padding, padding, padding, padding) - provideText(localizedText.noEventMessage, messageStack, textFormat.noEvents) - } - - // Whether or not we displayed something, return here. - return - } - - // Set up the event stack. - let eventStack = column.addStack() - eventStack.layoutVertically() - const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200 - eventStack.url = 'calshow:' + todaySeconds - - // If there are no events and we have a message, show it and return. - if (!eventData.eventsAreVisible && localizedText.noEventMessage.length) { - let message = provideText(localizedText.noEventMessage, eventStack, textFormat.noEvents) - eventStack.setPadding(padding, padding, padding, padding) - return - } - - // If we're not showing the message, don't pad the event stack. - eventStack.setPadding(0, 0, 0, 0) - - // Add each event to the stack. - var currentStack = eventStack - const futureEvents = eventData.futureEvents - for (let i = 0; i < futureEvents.length; i++) { - - const event = futureEvents[i] - const bottomPadding = (padding-10 < 0) ? 0 : padding-10 - - // If it's the tomorrow label, change to the tomorrow stack. - if (event.isLabel) { - let tomorrowStack = column.addStack() - tomorrowStack.layoutVertically() - const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800 - tomorrowStack.url = 'calshow:' + tomorrowSeconds - currentStack = tomorrowStack - - // Mimic the formatting of an event title, mostly. - const eventLabelStack = align(currentStack) - const eventLabel = provideText(event.title, eventLabelStack, textFormat.eventLabel) - eventLabelStack.setPadding(padding, padding, padding, padding) - continue - } - - const titleStack = align(currentStack) - titleStack.layoutHorizontally() - const showCalendarColor = eventSettings.showCalendarColor - const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle" - - // If we're showing a color, and it's not shown on the right, add it to the left. - if (showCalendarColor.length && !showCalendarColor.includes("right")) { - let colorItemText = provideTextSymbol(colorShape) + " " - let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) - colorItem.textColor = event.calendar.color - } - - const title = provideText(event.title.trim(), titleStack, textFormat.eventTitle) - titleStack.setPadding(padding, padding, event.isAllDay ? padding : padding/5, padding) - - // If we're showing a color on the right, show it. - if (showCalendarColor.length && showCalendarColor.includes("right")) { - let colorItemText = " " + provideTextSymbol(colorShape) - let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle) - colorItem.textColor = event.calendar.color - } - - // If there are too many events, limit the line height. - if (futureEvents.length >= 3) { title.lineLimit = 1 } - - // If it's an all-day event, we don't need a time. - if (event.isAllDay) { continue } - - // Format the time information. - let timeText = formatTime(event.startDate) - - // If we show the length as time, add an en dash and the time. - if (eventSettings.showEventLength == "time") { - timeText += "–" + formatTime(event.endDate) - - // If we should it as a duration, add the minutes. - } else if (eventSettings.showEventLength == "duration") { - const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60) - const hours = Math.floor(duration/60) - const minutes = Math.floor(duration % 60) - const hourText = hours>0 ? hours + localizedText.durationHour : "" - const minuteText = minutes>0 ? minutes + localizedText.durationMinute : "" - const showSpace = hourText.length && minuteText.length - timeText += " \u2022 " + hourText + (showSpace ? " " : "") + minuteText - } - - const timeStack = align(currentStack) - const time = provideText(timeText, timeStack, textFormat.eventTime) - timeStack.setPadding(0, padding, padding, padding) - } -} - -// Display the current weather. -async function current(column) { - - // Requirements: weather and sunrise - if (!weatherData) { await setupWeather() } - if (!sunData) { await setupSunrise() } - - // Set up the current weather stack. - let currentWeatherStack = column.addStack() - currentWeatherStack.layoutVertically() - currentWeatherStack.setPadding(0, 0, 0, 0) - currentWeatherStack.url = "https://weather.com/weather/today/l/" + locationData.latitude + "," + locationData.longitude - - // If we're showing the location, add it. - if (weatherSettings.showLocation) { - let locationTextStack = align(currentWeatherStack) - let locationText = provideText(locationData.locality, locationTextStack, textFormat.smallTemp) - locationTextStack.setPadding(padding, padding, padding, padding) - } - - // Show the current condition symbol. - let mainConditionStack = align(currentWeatherStack) - let mainCondition = mainConditionStack.addImage(provideConditionSymbol(weatherData.currentCondition,isNight(currentDate))) - mainCondition.imageSize = new Size(22,22) - mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : padding, padding, 0, padding) - - // If we're showing the description, add it. - if (weatherSettings.showCondition) { - let conditionTextStack = align(currentWeatherStack) - let conditionText = provideText(weatherData.currentDescription, conditionTextStack, textFormat.smallTemp) - conditionTextStack.setPadding(padding, padding, 0, padding) - } - - // Show the current temperature. - const tempStack = align(currentWeatherStack) - tempStack.setPadding(0, padding, 0, padding) - const tempText = Math.round(weatherData.currentTemp) + "°" - const temp = provideText(tempText, tempStack, textFormat.largeTemp) - - // If we're not showing the high and low, end it here. - if (!weatherSettings.showHighLow) { return } - - // Show the temp bar and high/low values. - let tempBarStack = align(currentWeatherStack) - tempBarStack.layoutVertically() - tempBarStack.setPadding(0, padding, padding, padding) - - let tempBar = drawTempBar() - let tempBarImage = tempBarStack.addImage(tempBar) - tempBarImage.size = new Size(50,0) - - tempBarStack.addSpacer(1) - - let highLowStack = tempBarStack.addStack() - highLowStack.layoutHorizontally() - - const mainLowText = Math.round(weatherData.todayLow).toString() - const mainLow = provideText(mainLowText, highLowStack, textFormat.tinyTemp) - highLowStack.addSpacer() - const mainHighText = Math.round(weatherData.todayHigh).toString() - const mainHigh = provideText(mainHighText, highLowStack, textFormat.tinyTemp) - - tempBarStack.size = new Size(60,30) -} - -// Display upcoming weather. -async function future(column) { - - // Requirements: weather and sunrise - if (!weatherData) { await setupWeather() } - if (!sunData) { await setupSunrise() } - - // Set up the future weather stack. - let futureWeatherStack = column.addStack() - futureWeatherStack.layoutVertically() - futureWeatherStack.setPadding(0, 0, 0, 0) - futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + locationData.latitude + "," + locationData.longitude - - // Determine if we should show the next hour. - const showNextHour = (currentDate.getHours() < weatherSettings.tomorrowShownAtHour) - - // Set the label value. - const subLabelStack = align(futureWeatherStack) - const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel - const subLabel = provideText(subLabelText, subLabelStack, textFormat.smallTemp) - subLabelStack.setPadding(0, padding, padding/2, padding) - - // Set up the sub condition stack. - let subConditionStack = align(futureWeatherStack) - subConditionStack.layoutHorizontally() - subConditionStack.centerAlignContent() - subConditionStack.setPadding(0, padding, padding, padding) - - // Determine if it will be night in the next hour. - var nightCondition - if (showNextHour) { - const addHour = currentDate.getTime() + (60*60*1000) - const newDate = new Date(addHour) - nightCondition = isNight(newDate) - } else { - nightCondition = false - } - - let subCondition = subConditionStack.addImage(provideConditionSymbol(showNextHour ? weatherData.nextHourCondition : weatherData.tomorrowCondition,nightCondition)) - const subConditionSize = showNextHour ? 14 : 18 - subCondition.imageSize = new Size(subConditionSize, subConditionSize) - subConditionStack.addSpacer(5) - - // The next part of the display changes significantly for next hour vs tomorrow. - if (showNextHour) { - const subTempText = Math.round(weatherData.nextHourTemp) + "°" - const subTemp = provideText(subTempText, subConditionStack, textFormat.smallTemp) - - } else { - let tomorrowLine = subConditionStack.addImage(drawVerticalLine(new Color("ffffff", 0.5), 20)) - tomorrowLine.imageSize = new Size(3,28) - subConditionStack.addSpacer(5) - let tomorrowStack = subConditionStack.addStack() - tomorrowStack.layoutVertically() - - const tomorrowHighText = Math.round(weatherData.tomorrowHigh) + "" - const tomorrowHigh = provideText(tomorrowHighText, tomorrowStack, textFormat.tinyTemp) - tomorrowStack.addSpacer(4) - const tomorrowLowText = Math.round(weatherData.tomorrowLow) + "" - const tomorrowLow = provideText(tomorrowLowText, tomorrowStack, textFormat.tinyTemp) - } -} - -// Return a text-creation function. -function text(input = null) { - - function displayText(column) { - - // Don't do anything if the input is blank. - if (!input || input == "") { return } - - // Otherwise, add the text. - const textStack = align(column) - textStack.setPadding(padding, padding, padding, padding) - const textDisplay = provideText(input, textStack, textFormat.customText) - } - return displayText -} - -// Add a battery element to the widget; consisting of a battery icon and percentage. -async function battery(column) { - - // Get battery level via Scriptable function and format it in a convenient way - function getBatteryLevel() { - - const batteryLevel = Device.batteryLevel() - const batteryPercentage = `${Math.round(batteryLevel * 100)}%` - - return batteryPercentage - } - - const batteryLevel = Device.batteryLevel() - - // Set up the battery level item - let batteryStack = align(column) - batteryStack.layoutHorizontally() - batteryStack.centerAlignContent() - - let batteryIcon = batteryStack.addImage(provideBatteryIcon()) - batteryIcon.imageSize = new Size(30,30) - - // Change the battery icon to red if battery level is <= 20 to match system behavior - if ( Math.round(batteryLevel * 100) > 20 || Device.isCharging() ) { - - batteryIcon.tintColor = new Color(textFormat.battery.color || textFormat.defaultText.color) - - } else { - - batteryIcon.tintColor = Color.red() - - } - - batteryStack.addSpacer(padding * 0.6) - - // Display the battery status - let batteryInfo = provideText(getBatteryLevel(), batteryStack, textFormat.battery) - - batteryStack.setPadding(padding/2, padding, padding/2, padding) - -} - -// Show the sunrise or sunset time. -async function sunrise(column) { - - // Requirements: sunrise - if (!sunData) { await setupSunrise() } - - const sunrise = sunData.sunrise - const sunset = sunData.sunset - const showWithin = sunriseSettings.showWithin - const closeToSunrise = closeTo(sunrise) <= showWithin - const closeToSunset = closeTo(sunset) <= showWithin - - // If we only show sometimes and we're not close, return. - if (showWithin > 0 && !closeToSunrise && !closeToSunset) { return } - - // Otherwise, determine which time to show. - const showSunrise = closeTo(sunrise) <= closeTo(sunset) - - // Set up the stack. - const sunriseStack = align(column) - sunriseStack.setPadding(padding/2, padding, padding/2, padding) - sunriseStack.layoutHorizontally() - sunriseStack.centerAlignContent() - - sunriseStack.addSpacer(padding * 0.3) - - // Add the correct symbol. - const symbolName = showSunrise ? "sunrise.fill" : "sunset.fill" - const symbol = sunriseStack.addImage(SFSymbol.named(symbolName).image) - symbol.imageSize = new Size(22,22) - - sunriseStack.addSpacer(padding) - - // Add the time. - const timeText = formatTime(showSunrise ? new Date(sunrise) : new Date(sunset)) - const time = provideText(timeText, sunriseStack, textFormat.sunrise) -} - -// Allow for either term to be used. -async function sunset(column) { - return await sunrise(column) -} - -/* - * HELPER FUNCTIONS - * These functions perform duties for other functions. - * =================================================== - */ - -// Determines if the provided date is at night. -function isNight(dateInput) { - const timeValue = dateInput.getTime() - return (timeValue < sunData.sunrise) || (timeValue > sunData.sunset) -} - -// Determines if two dates occur on the same day -function sameDay(d1, d2) { - return d1.getFullYear() === d2.getFullYear() && - d1.getMonth() === d2.getMonth() && - d1.getDate() === d2.getDate() -} - -// Returns the number of minutes between now and the provided date. -function closeTo(time) { - return Math.abs(currentDate.getTime() - time) / 60000 -} - -// Format the time for a Date input. -function formatTime(date) { - let df = new DateFormatter() - df.locale = locale - df.useNoDateStyle() - df.useShortTimeStyle() - return df.string(date) -} - -// Provide a text symbol with the specified shape. -function provideTextSymbol(shape) { - - // Rectangle character. - if (shape.startsWith("rect")) { - return "\u2759" - } - // Circle character. - if (shape == "circle") { - return "\u2B24" - } - // Default to the rectangle. - return "\u2759" -} - -// Provide a battery SFSymbol with accurate level drawn on top of it. -function provideBatteryIcon() { - - // If we're charging, show the charging icon. - if (Device.isCharging()) { return SFSymbol.named("battery.100.bolt").image } - - // Set the size of the battery icon. - const batteryWidth = 87 - const batteryHeight = 41 - - // Start our draw context. - let draw = new DrawContext() - draw.opaque = false - draw.respectScreenScale = true - draw.size = new Size(batteryWidth, batteryHeight) - - // Draw the battery. - draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight)) - - // Match the battery level values to the SFSymbol. - const x = batteryWidth*0.1525 - const y = batteryHeight*0.247 - const width = batteryWidth*0.602 - const height = batteryHeight*0.505 - - // Prevent unreadable icons. - let level = Device.batteryLevel() - if (level < 0.05) { level = 0.05 } - - // Determine the width and radius of the battery level. - const current = width * level - let radius = height/6.5 - - // When it gets low, adjust the radius to match. - if (current < (radius * 2)) { radius = current / 2 } - - // Make the path for the battery level. - let barPath = new Path() - barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius) - draw.addPath(barPath) - draw.setFillColor(Color.black()) - draw.fillPath() - return draw.getImage() -} - -// Provide a symbol based on the condition. -function provideConditionSymbol(cond,night) { - - // Define our symbol equivalencies. - let symbols = { - - // Thunderstorm - "2": function() { return "cloud.bolt.rain.fill" }, - - // Drizzle - "3": function() { return "cloud.drizzle.fill" }, - - // Rain - "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" }, - - // Snow - "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" }, - - // Atmosphere - "7": function() { - if (cond == 781) { return "tornado" } - if (cond == 701 || cond == 741) { return "cloud.fog.fill" } - return night ? "cloud.fog.fill" : "sun.haze.fill" - }, - - // Clear and clouds - "8": function() { - if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" } - if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" } - return "cloud.fill" - } - } - - // Find out the first digit. - let conditionDigit = Math.floor(cond / 100) - - // Get the symbol. - return SFSymbol.named(symbols[conditionDigit]()).image -} - -// Provide a font based on the input. -function provideFont(fontName, fontSize) { - const fontGenerator = { - "ultralight": function() { return Font.ultraLightSystemFont(fontSize) }, - "light": function() { return Font.lightSystemFont(fontSize) }, - "regular": function() { return Font.regularSystemFont(fontSize) }, - "medium": function() { return Font.mediumSystemFont(fontSize) }, - "semibold": function() { return Font.semiboldSystemFont(fontSize) }, - "bold": function() { return Font.boldSystemFont(fontSize) }, - "heavy": function() { return Font.heavySystemFont(fontSize) }, - "black": function() { return Font.blackSystemFont(fontSize) }, - "italic": function() { return Font.italicSystemFont(fontSize) } - } - - const systemFont = fontGenerator[fontName] - if (systemFont) { return systemFont() } - return new Font(fontName, fontSize) -} - -// Add formatted text to a container. -function provideText(string, container, format) { - const textItem = container.addText(string) - const textFont = format.font || textFormat.defaultText.font - const textSize = format.size || textFormat.defaultText.size - const textColor = format.color || textFormat.defaultText.color - - textItem.font = provideFont(textFont, textSize) - textItem.textColor = new Color(textColor) - return textItem -} - -/* - * DRAWING FUNCTIONS - * These functions draw onto a canvas. - * =================================== - */ - -// Draw the vertical line in the tomorrow view. -function drawVerticalLine(color, height) { - - const width = 2 - - let draw = new DrawContext() - draw.opaque = false - draw.respectScreenScale = true - draw.size = new Size(width,height) - - let barPath = new Path() - const barHeight = height - barPath.addRoundedRect(new Rect(0, 0, width, height), width/2, width/2) - draw.addPath(barPath) - draw.setFillColor(color) - draw.fillPath() - - return draw.getImage() -} - -// Draw the temp bar. -function drawTempBar() { - - // Set the size of the temp bar. - const tempBarWidth = 200 - const tempBarHeight = 20 - - // Calculate the current percentage of the high-low range. - let percent = (weatherData.currentTemp - weatherData.todayLow) / (weatherData.todayHigh - weatherData.todayLow) - - // If we're out of bounds, clip it. - if (percent < 0) { - percent = 0 - } else if (percent > 1) { - percent = 1 - } - - // Determine the scaled x-value for the current temp. - const currPosition = (tempBarWidth - tempBarHeight) * percent - - // Start our draw context. - let draw = new DrawContext() - draw.opaque = false - draw.respectScreenScale = true - draw.size = new Size(tempBarWidth, tempBarHeight) - - // Make the path for the bar. - let barPath = new Path() - const barHeight = tempBarHeight - 10 - barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2) - draw.addPath(barPath) - draw.setFillColor(new Color("ffffff", 0.5)) - draw.fillPath() - - // Make the path for the current temp indicator. - let currPath = new Path() - currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight)) - draw.addPath(currPath) - draw.setFillColor(new Color("ffffff", 1)) - draw.fillPath() - - return draw.getImage() -} From ad3c0c748c6b9640adbf3ed98dfbbcf2d9e68b44 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:44:52 +0300 Subject: [PATCH 14/21] Delete WallabagUnread.js --- scripts/WallabagUnread.js | 104 -------------------------------------- 1 file changed, 104 deletions(-) delete mode 100644 scripts/WallabagUnread.js diff --git a/scripts/WallabagUnread.js b/scripts/WallabagUnread.js deleted file mode 100644 index 9f0af39..0000000 --- a/scripts/WallabagUnread.js +++ /dev/null @@ -1,104 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: light-gray; icon-glyph: book; - -/** - * WIDGET CONFIGURATION - */ -const LIGHT_BG_COLOUR = '#F1F1F1' -const DARK_BG_COLOUR = '#F1F1F1' -const INSTAPAPER_RSS_FEED_URL = "" - -const data = await fetchData() -const widget = await createWidget(data) - -// Check if the script is running in -// a widget. If not, show a preview of -// the widget to easier debug it. -if (!config.runsInWidget) { - await widget.presentSmall() -} -// Tell the system to show the widget. -Script.setWidget(widget) -Script.complete() - -async function createWidget(data) { - const gradientBg = [ - new Color(`${LIGHT_BG_COLOUR}D9`), - new Color(`${DARK_BG_COLOUR}D9`), - ] - const gradient = new LinearGradient() - gradient.locations = [0, 1] - gradient.colors = gradientBg - const bg = new Color(LIGHT_BG_COLOUR) - const logoReq = await new Request('https://i.imgur.com/xiKYzQP.png') - const logoImg = await logoReq.loadImage() - - const w = new ListWidget() - w.useDefaultPadding() - w.backgroundColor = bg - w.backgroundGradient = gradient - - const itemFontSize = config.widgetFamily === 'large' ? 15 : 12 - - const headerRow = w.addStack() - headerRow.layoutHorizontally() - - const wimg = headerRow.addImage(logoImg) - wimg.imageSize = new Size(18, 18) - headerRow.addSpacer(10) - - const title = - config.widgetFamily === 'small' ? 'Wallabag' : 'Wallabag: Unread' - const headerTitle = headerRow.addText(title) - headerTitle.font = Font.semiboldSystemFont(15) - headerTitle.textColor = Color.black() - headerTitle.textOpacity = 0.9 - - w.addSpacer(10) - - const widgetCount = config.widgetFamily === 'large' ? 7 : 3 - data.forEach(({ title, link }, index) => { - if (index > widgetCount) { - return - } - const itemTitle = w.addText(title[0]) - itemTitle.font = Font.systemFont(itemFontSize) - itemTitle.textColor = Color.black() - itemTitle.textOpacity = 0.9 - itemTitle.url = link[0] - - if (index < widgetCount) { - const spacing = config.widgetFamily === 'large' ? 8 : 6 - w.addSpacer(spacing) - } - }) - - return w -} - -async function fetch(url) { - const req = new Request(url) - const json = await req.loadJSON() - return json -} - -async function fetchData() { - const unreadData = await fetch( - `https://rsstojson.com/v1/api/?rss_url=${INSTAPAPER_RSS_FEED_URL}` - ) - - return unreadData.rss.channel[0].item -} - -function addSymbol({ - symbol = 'applelogo', - stack, - color = Color.black(), - size = 20, -}) { - const _sym = SFSymbol.named(symbol) - const wImg = stack.addImage(_sym.image) - wImg.tintColor = color - wImg.imageSize = new Size(size, size) -} From 218a802bd8f42922b9ccc5b86db1ab150292d6a1 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:44:59 +0300 Subject: [PATCH 15/21] Delete Vultr Bandwidth .js --- scripts/Vultr Bandwidth .js | 74 ------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 scripts/Vultr Bandwidth .js diff --git a/scripts/Vultr Bandwidth .js b/scripts/Vultr Bandwidth .js deleted file mode 100644 index 6a98e7f..0000000 --- a/scripts/Vultr Bandwidth .js +++ /dev/null @@ -1,74 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: deep-gray; icon-glyph: magic; - -let widget = await createWidget(); -Script.setWidget(widget); - - -async function createWidget() { - const url = "https://api.vultr.com/v1/server/list" - let req = new Request(url); - req.method = 'GET'; - let APIKEY = ""; - req.headers = { - "API-Key": APIKEY, - "Content-Type": "application/json", - "Accept": "application/json" - }; - const data = await req.loadJSON(); - let widgetHeader = "vultr" - let CLOUD = data[0].current_bandwidth_gb; - let VPN = data[0].current_bandwidth_gb; - let YTDL = data[0].current_bandwidth_gb; - console.log(CLOUD) - console.log(VPN) - console.log(YTDL) - let w = new ListWidget(); - let titleTxt = w.addText(widgetHeader); - titleTxt.centerAlignText(16); - let bodyTxt0 = w.addText("vpn " + VPN + " GB"); - let bodyTxt1 = w.addText("Cloud " + CLOUD + " GB"); - let bodyTxt2 = w.addText("YTDL " + YTDL + " GB"); - // w.backgroundColor = new Color("#1b1c1f"); -const files = FileManager.local(); -const forceImageUpdate = true; -const imageBackground = true; - -if (imageBackground) { - - // Determine if our image exists and when it was saved. - const path = files.joinPath(files.documentsDirectory(), "IPsWidget-cache") - const exists = files.fileExists(path) - - // If it exists and an update isn't forced, use the cache. - if (exists && (config.runsInWidget || !forceImageUpdate)) { - w.backgroundImage = files.readImage(path) - - // If it's missing when running in the widget, use a gray background. - } else if (!exists && config.runsInWidget) { - w.backgroundColor = Color.gray() - - // But if we're running in app, prompt the user for the image. - } else { - const img = await Photos.fromLibrary() - w.backgroundImage = img - files.writeImage(path, img) - }; - - // If it's not an image background, show the gradient. - } else { - let gradient = new LinearGradient() - let gradientSettings = await setupGradient() - - gradient.colors = gradientSettings.color() - gradient.locations = gradientSettings.position() - - w.backgroundGradient = gradient - }; - - // Finish the widget and show a preview. - Script.setWidget(w); - - return w; -}; From 5153681f84935d827968cd82289449d63efb5347 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:45:09 +0300 Subject: [PATCH 16/21] Delete TermiWidget.js --- scripts/TermiWidget.js | 273 ----------------------------------------- 1 file changed, 273 deletions(-) delete mode 100644 scripts/TermiWidget.js diff --git a/scripts/TermiWidget.js b/scripts/TermiWidget.js deleted file mode 100644 index b3fb5ab..0000000 --- a/scripts/TermiWidget.js +++ /dev/null @@ -1,273 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: deep-gray; icon-glyph: magic; - -// Change these to your usernames! -const user = "evan"; - -// API PARAMETERS !important -// WEATHER_API_KEY, you need an Open Weather API Key -// You can get one for free at: https://home.openweathermap.org/api_keys (account needed). -const WEATHER_API_KEY = ""; -const DEFAULT_LOCATION = { - latitude: 0, - longitude: 0 -}; -const TAUTULLI_API_BASE = ""; -const TAUTULLI_API_KEY = ""; -const HOME_ASSISTANT_API_BASE = ""; -const HOME_ASSISTANT_API_KEY = ""; -const UPCOMING_SAT_PASS_URL = ""; - -const Cache = importModule('cache'); - -const cache = new Cache("termiWidgetCache"); -const data = await fetchData(); -const widget = createWidget(data); -Script.setWidget(widget); -Script.complete(); - -function createWidget(data) { - console.log(data) - const w = new ListWidget() - const bgColor = new LinearGradient() - bgColor.colors = [new Color("#29323c"), new Color("#1c1c1c")] - bgColor.locations = [0.0, 1.0] - w.backgroundGradient = bgColor - w.setPadding(12, 15, 15, 12) - - const stack = w.addStack(); - stack.layoutHorizontally(); - - const leftStack = stack.addStack(); - leftStack.layoutVertically(); - leftStack.spacing = 6; - leftStack.size = new Size(200, 0); - - const time = new Date() - const dfTime = new DateFormatter() - dfTime.locale = "en" - dfTime.useMediumDateStyle() - dfTime.useNoTimeStyle() - - const firstLine = leftStack.addText(`[] ${user} ~$ now`) - firstLine.textColor = Color.white() - firstLine.textOpacity = 0.7 - firstLine.font = new Font("Menlo", 11) - - const timeLine = leftStack.addText(`[🗓] ${dfTime.string(time)}`) - timeLine.textColor = Color.white() - timeLine.font = new Font("Menlo", 11) - - const batteryLine = leftStack.addText(`[🔋] ${renderBattery()}`) - batteryLine.textColor = new Color("#6ef2ae") - batteryLine.font = new Font("Menlo", 11) - - const locationLine = leftStack.addText(`[️️📍] Location: ${data.weather.location}`) - locationLine.textColor = new Color("#7dbbae") - locationLine.font = new Font("Menlo", 11) - - const homeLine = leftStack.addText(`[🏠] ${data.home.mode}, ${data.home.temperature}°, Lights ${data.home.lights ? "On" : "Off"}`); - homeLine.textColor = new Color("#ff9468") - homeLine.font = new Font("Menlo", 11) - - let plexText = `[🍿] Plex: ${data.plex.streams} stream${data.plex.streams == 1 ? '' : 's'}`; - // if (data.plex.streams > 0) { - // plexText += `, ${data.plex.transcodes} transcode${data.plex.transcodes == 1 ? '' : 's'}`; - // } - const plexLine = leftStack.addText(plexText); - plexLine.textColor = new Color("#ffa7d3") - plexLine.font = new Font("Menlo", 11) - - const satLine = leftStack.addText(`[🛰] ${data.satPass}`); - satLine.textColor = new Color("#ffcc66") - satLine.font = new Font("Menlo", 11) - - stack.addSpacer(); - const rightStack = stack.addStack(); - rightStack.spacing = 2; - rightStack.layoutVertically(); - rightStack.bottomAlignContent(); - - addWeatherLine(rightStack, data.weather.icon, 32); - addWeatherLine(rightStack, `${data.weather.description}, ${data.weather.temperature}°`, 12, true); - addWeatherLine(rightStack, `High: ${data.weather.high}°`); - addWeatherLine(rightStack, `Low: ${data.weather.low}°`); - addWeatherLine(rightStack, `Wind: ${data.weather.wind} mph`); - - return w -} - -function addWeatherLine(w, text, size, bold) { - const stack = w.addStack(); - stack.setPadding(0, 0, 0, 0); - stack.layoutHorizontally(); - stack.addSpacer(); - const line = stack.addText(text); - line.textColor = new Color("#ffcc66"); - line.font = new Font("Menlo" + (bold ? "-Bold" : ""), size || 11); -} - -async function fetchData() { - const weather = await fetchWeather(); - const plex = await fetchPlex(); - const home = await fetchHome(); - const satPass = await fetchNextSatPass(); - - return { - weather, - plex, - home, - satPass, - } -} - -function renderBattery() { - const batteryLevel = Device.batteryLevel() - const juice = "#".repeat(Math.floor(batteryLevel * 8)) - const used = ".".repeat(8 - juice.length) - const batteryAscii = `[${juice}${used}] ${Math.round(batteryLevel * 100)}%` - return batteryAscii -} - -async function fetchWeather() { - let location = await cache.read('location'); - if (!location) { - try { - Location.setAccuracyToThreeKilometers(); - location = await Location.current(); - } catch(error) { - location = await cache.read('location'); - } - } - if (!location) { - location = DEFAULT_LOCATION; - } - const address = await Location.reverseGeocode(location.latitude, location.longitude); - const url = "https://api.openweathermap.org/data/2.5/onecall?lat=" + location.latitude + "&lon=" + location.longitude + "&exclude=minutely,hourly,alerts&units=imperial&lang=en&appid=" + WEATHER_API_KEY; - const data = await fetchJson(`weather_${address[0].locality}`, url); - - return { - location: address[0].locality, - icon: getWeatherEmoji(data.weather[0].id, ((new Date()).getTime() / 1000) >= data.sys.sunset), - description: data.weather[0].main, - temperature: Math.round(data.main.temp), - wind: Math.round(data.wind.speed), - high: Math.round(data.main.temp_max), - low: Math.round(data.main.temp_min), - } -} - -async function fetchPlex() { - const url = `${TAUTULLI_API_BASE}/api/v2?apikey=${TAUTULLI_API_KEY}&cmd=get_activity`; - const data = await fetchJson(`plex`, url); - - return { - streams: data.response.data.stream_count, - transcodes: data.response.data.stream_count_transcode, - }; -} - -async function fetchHome() { - const mode = await fetchHomeAssistant('states/input_select.mode'); - const temp = await fetchHomeAssistant('states/sensor.hallway_temperature'); - const livingRoomLight = (await fetchHomeAssistant('states/light.living_room')).state == "on"; - const bedRoomLight = (await fetchHomeAssistant('states/light.bedroom')).state == "on"; - const hallwayLight = (await fetchHomeAssistant('states/light.hallway')).state == "on"; - const bathroomLight = (await fetchHomeAssistant('states/light.bathroom')).state == "on"; - - return { - mode: mode.state, - temperature: Math.round(parseFloat(temp.state)), - lights: livingRoomLight || bedRoomLight || hallwayLight || bathroomLight, - }; -} - -async function fetchHomeAssistant(path) { - return fetchJson(path, `${HOME_ASSISTANT_API_BASE}/api/${path}`, { - 'Authorization': `Bearer ${HOME_ASSISTANT_API_KEY}`, - 'Content-Type': 'application/json', - }); -} - -async function fetchNextSatPass() { - const passes = await fetchJson('upcoming-passes.json', UPCOMING_SAT_PASS_URL); - const now = new Date(); - const nextPass = passes - .filter((p) => now.getTime() < p.end)[0]; - - if (!nextPass) { - return 'No more passes today'; - } - - if (nextPass.start > now.getTime()) { - const minutes = Math.round(((nextPass.start - now.getTime()) / 1000) / 60); - const hours = Math.round((((nextPass.start - now.getTime()) / 1000) / 60) / 60); - - if (minutes > 59) { - return `${nextPass.satellite} in ${hours}h, ${Math.round(nextPass.elevation)}°`; - } else { - return `${nextPass.satellite} in ${minutes}m, ${Math.round(nextPass.elevation)}°`; - } - } else { - return `${nextPass.satellite} for ${Math.round(((nextPass.end - now.getTime()) / 1000) / 60)}m, ${Math.round(nextPass.elevation)}°`; - } -} - -async function fetchJson(key, url, headers) { - const cached = await cache.read(key, 5); - if (cached) { - return cached; - } - - try { - console.log(`Fetching url: ${url}`); - const req = new Request(url); - req.headers = headers; - const resp = await req.loadJSON(); - cache.write(key, resp); - return resp; - } catch (error) { - try { - return cache.read(key, 5); - } catch (error) { - console.log(`Couldn't fetch ${url}`); - } - } -} - -function getWeatherEmoji(code, isNight) { - if (code >= 200 && code < 300 || code == 960 || code == 961) { - return "⛈" - } else if ((code >= 300 && code < 600) || code == 701) { - return "🌧" - } else if (code >= 600 && code < 700) { - return "❄️" - } else if (code == 711) { - return "🔥" - } else if (code == 800) { - return isNight ? "🌕" : "☀️" - } else if (code == 801) { - return isNight ? "☁️" : "🌤" - } else if (code == 802) { - return isNight ? "☁️" : "⛅️" - } else if (code == 803) { - return isNight ? "☁️" : "🌥" - } else if (code == 804) { - return "☁️" - } else if (code == 900 || code == 962 || code == 781) { - return "🌪" - } else if (code >= 700 && code < 800) { - return "🌫" - } else if (code == 903) { - return "🥶" - } else if (code == 904) { - return "🥵" - } else if (code == 905 || code == 957) { - return "💨" - } else if (code == 906 || code == 958 || code == 959) { - return "🧊" - } else { - return "❓" - } -} From 65ae73380cdb827380ac88c452a956f172695684 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:45:17 +0300 Subject: [PATCH 17/21] Delete InstapaperUnread.js --- scripts/InstapaperUnread.js | 104 ------------------------------------ 1 file changed, 104 deletions(-) delete mode 100644 scripts/InstapaperUnread.js diff --git a/scripts/InstapaperUnread.js b/scripts/InstapaperUnread.js deleted file mode 100644 index 0039435..0000000 --- a/scripts/InstapaperUnread.js +++ /dev/null @@ -1,104 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: light-gray; icon-glyph: book; - -/** - * WIDGET CONFIGURATION - */ -const LIGHT_BG_COLOUR = '#F1F1F1' -const DARK_BG_COLOUR = '#F1F1F1' -const INSTAPAPER_RSS_FEED_URL = "" - -const data = await fetchData() -const widget = await createWidget(data) - -// Check if the script is running in -// a widget. If not, show a preview of -// the widget to easier debug it. -if (!config.runsInWidget) { - await widget.presentSmall() -} -// Tell the system to show the widget. -Script.setWidget(widget) -Script.complete() - -async function createWidget(data) { - const gradientBg = [ - new Color(`${LIGHT_BG_COLOUR}D9`), - new Color(`${DARK_BG_COLOUR}D9`), - ] - const gradient = new LinearGradient() - gradient.locations = [0, 1] - gradient.colors = gradientBg - const bg = new Color(LIGHT_BG_COLOUR) - const logoReq = await new Request('https://i.imgur.com/BKjVm7c.png') - const logoImg = await logoReq.loadImage() - - const w = new ListWidget() - w.useDefaultPadding() - w.backgroundColor = bg - w.backgroundGradient = gradient - - const itemFontSize = config.widgetFamily === 'large' ? 15 : 12 - - const headerRow = w.addStack() - headerRow.layoutHorizontally() - - const wimg = headerRow.addImage(logoImg) - wimg.imageSize = new Size(18, 18) - headerRow.addSpacer(10) - - const title = - config.widgetFamily === 'small' ? 'Instapaper' : 'Instapaper: Unread' - const headerTitle = headerRow.addText(title) - headerTitle.font = Font.semiboldSystemFont(15) - headerTitle.textColor = Color.black() - headerTitle.textOpacity = 0.9 - - w.addSpacer(10) - - const widgetCount = config.widgetFamily === 'large' ? 7 : 3 - data.forEach(({ title, link }, index) => { - if (index > widgetCount) { - return - } - const itemTitle = w.addText(title[0]) - itemTitle.font = Font.systemFont(itemFontSize) - itemTitle.textColor = Color.black() - itemTitle.textOpacity = 0.9 - itemTitle.url = link[0] - - if (index < widgetCount) { - const spacing = config.widgetFamily === 'large' ? 8 : 6 - w.addSpacer(spacing) - } - }) - - return w -} - -async function fetch(url) { - const req = new Request(url) - const json = await req.loadJSON() - return json -} - -async function fetchData() { - const unreadData = await fetch( - `https://rsstojson.com/v1/api/?rss_url=${INSTAPAPER_RSS_FEED_URL}` - ) - - return unreadData.rss.channel[0].item -} - -function addSymbol({ - symbol = 'applelogo', - stack, - color = Color.black(), - size = 20, -}) { - const _sym = SFSymbol.named(symbol) - const wImg = stack.addImage(_sym.image) - wImg.tintColor = color - wImg.imageSize = new Size(size, size) -} From 96a28265d8983513d5c8a25cca68b90fd3ecf71f Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:45:24 +0300 Subject: [PATCH 18/21] Delete IPsWidget.js --- scripts/IPsWidget.js | 85 -------------------------------------------- 1 file changed, 85 deletions(-) delete mode 100644 scripts/IPsWidget.js diff --git a/scripts/IPsWidget.js b/scripts/IPsWidget.js deleted file mode 100644 index 3ef9b5b..0000000 --- a/scripts/IPsWidget.js +++ /dev/null @@ -1,85 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: deep-gray; icon-glyph: wifi; -let widget = await createWidget(); -Script.setWidget(widget); - -async function createWidget() { - - -// CloudFlare -const url = "" -let req = new Request(url); -req.method = 'GET'; -let authHeader = "" -let authHeader2 = "" -req.headers = { - "X-Auth-Email": authHeader2, - "X-Auth-Key": authHeader, - "Content-Type": "application/json", - "Accept": "application/json" -}; -const data = await req.loadJSON(); -let CFIP = data.result.content; - -// IPINFO.IO -const reqq = new Request("https://ipinfo.io/json"); -const dataa = await reqq.loadJSON(); -let IPNOW = dataa.ip - if (IPNOW == "") { - IP = "VPN ON" - } else { - IP = dataa.ip - }; - -// Widget Constraction -let w = new ListWidget(); -let WidgetHeader = "IPCenter"; -let titleTxt = w.addText(WidgetHeader); -titleTxt.centerAlignText(20); -let bodyTxt = w.addText("CloudFlare: " + CFIP); -let bodyTxtt = w.addText("MyIP: " + IP) -// w.backgroundColor = new Color("#1b1c1f"); -const files = FileManager.local() - -const forceImageUpdate = false -const imageBackground = true - -if (imageBackground) { - - // Determine if our image exists and when it was saved. - const path = files.joinPath(files.documentsDirectory(), "IPsWidget-cache") - const exists = files.fileExists(path) - - // If it exists and an update isn't forced, use the cache. - if (exists && (config.runsInWidget || !forceImageUpdate)) { - w.backgroundImage = files.readImage(path) - - // If it's missing when running in the widget, use a gray background. - } else if (!exists && config.runsInWidget) { - w.backgroundColor = Color.gray() - - // But if we're running in app, prompt the user for the image. - } else { - const img = await Photos.fromLibrary() - w.backgroundImage = img - files.writeImage(path, img) - } - - // If it's not an image background, show the gradient. - } else { - let gradient = new LinearGradient() - let gradientSettings = await setupGradient() - - gradient.colors = gradientSettings.color() - gradient.locations = gradientSettings.position() - - w.backgroundGradient = gradient - } - - // Finish the widget and show a preview. - Script.setWidget(w) - -return w; - -}; From 79e6d000a3add46c58d40ba829598f123bd16f7e Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:45:33 +0300 Subject: [PATCH 19/21] Delete Hello.js --- scripts/Hello.js | 121 ----------------------------------------------- 1 file changed, 121 deletions(-) delete mode 100644 scripts/Hello.js diff --git a/scripts/Hello.js b/scripts/Hello.js deleted file mode 100644 index a3bfb73..0000000 --- a/scripts/Hello.js +++ /dev/null @@ -1,121 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: gray; icon-glyph: grin-tongue-wink; -// To use, add a parameter to the widget with a format of: image.png|padding-top|text-color -// The image should be placed in the iCloud Scriptable folder (case-sensitive). -// The padding-top spacing parameter moves the text down by a set amount. -// The text color parameter should be a hex value. - -// For example, to use the image bkg_fall.PNG with a padding of 40 and a text color of red, -// the parameter should be typed as: bkg_fall.png|40|#ff0000 - -// All parameters are required and separated with "|" - -let widgetHello = new ListWidget(); -var today = new Date(); - -var widgetInputRAW = args.widgetParameter; - -try { - widgetInputRAW.toString(); -} catch(e) { - throw new Error("Please long press the widget and add a parameter."); -} - -var widgetInput = widgetInputRAW.toString(); - -var inputArr = widgetInput.split("|"); - -var scriptableFilePath = "/var/mobile/Library/Mobile Documents/iCloud~dk~simonbs~Scriptable/Documents/"; -var removeSpaces1 = inputArr[0].split(" "); -var removeSpaces2 = removeSpaces1.join(''); -var tempPath = removeSpaces2.split("."); -var backgroundImageURLRAW = scriptableFilePath + tempPath[0]; - -var fileTypes = ['png', 'jpg', 'jpeg', 'tiff', 'webp']; - -var fm = FileManager.iCloud(); -var backgroundImageURL = scriptableFilePath + tempPath[0] + "."; - -fileTypes.forEach(function(item) { - if (fm.fileExists((backgroundImageURL + item.toLowerCase())) == true) { - backgroundImageURL = backgroundImageURLRAW + "." + item.toLowerCase(); - } else if (fm.fileExists((backgroundImageURL + item.toUpperCase())) == true) { - backgroundImageURL = backgroundImageURLRAW + "." + item.toUpperCase(); - } -}); - -var spacing = parseInt(inputArr[1]); - - -var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; -var months = ['January','February','March','April','May','June','July','August','September','October','November','December']; - -var greetingsMorning = [ -'Good morning,' -]; -var greetingsAfternoon = [ -'Good afternoon,' -]; -var greetingsEvening = [ -'Good evening,' -]; -var greetingsNight = [ -'Bedtime,' -]; -var greetingsLateNight = [ -'Go to sleep!,' -]; - -var weekday = days[ today.getDay() ]; -var month = months[ today.getMonth() ]; -var date = today.getDate(); -var hour = today.getHours(); - -var datefull = weekday + ", " + month + " " + date; - -function randomGreeting(greetingArray) { - return Math.floor(Math.random() * greetingArray.length); -} - -var greeting = new String("Howdy.") -// var greeting = "Howdy."; -if (hour < 5 && hour >= 1) { - greeting = greetingsLateNight[randomGreeting(greetingsLateNight)]; -} else if (hour >= 23 || hour < 1) { - greeting = greetingsNight[randomGreeting(greetingsNight)]; -} else if (hour < 12) { - greeting = greetingsMorning[randomGreeting(greetingsMorning)]; -} else if (hour >= 12 && hour <= 17) { - greeting = greetingsAfternoon[randomGreeting(greetingsAfternoon)]; -} else if (hour > 17 && hour < 23) { - greeting = greetingsEvening[randomGreeting(greetingsEvening)]; -} - -if (month == "October" && date == 31) { - greeting = "Happy Halloween!"; -} -if (month == "December" && date == 25) { - greeting = "Merry Christmas!"; -} - -try { - inputArr[2].toString(); -} catch(e) { - throw new Error("Please long press the widget and add a parameter."); -} - -let themeColor = new Color(inputArr[2].toString()); - -widgetHello.addSpacer(parseInt(spacing)); -let hello = widgetHello.addText(greeting + " Abdulrahman."); -hello.font = Font.boldSystemFont(23); -hello.textColor = themeColor; -let datetext = widgetHello.addText(datefull); -datetext.font = Font.regularSystemFont(18); -datetext.textColor = themeColor; -widgetHello.addSpacer(); -widgetHello.setPadding(15, 7, 10, 0) -widgetHello.backgroundImage = Image.fromFile(backgroundImageURL); - -Script.setWidget(widgetHello); From de6b0d665258511859b94ec75aa0c1f7520b0ba2 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:46:00 +0300 Subject: [PATCH 20/21] Delete Coronavirus.js --- scripts/Coronavirus.js | 63 ------------------------------------------ 1 file changed, 63 deletions(-) delete mode 100644 scripts/Coronavirus.js diff --git a/scripts/Coronavirus.js b/scripts/Coronavirus.js deleted file mode 100644 index 75e4666..0000000 --- a/scripts/Coronavirus.js +++ /dev/null @@ -1,63 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: deep-green; icon-glyph: user-md; -// change "country" to a value from https://coronavirus-19-api.herokuapp.com/countries/ -const country = "" -const url = `https://coronavirus-19-api.herokuapp.com/countries/${country}` -const req = new Request(url) -const res = await req.loadJSON() - -if (config.runsInWidget) { - // create and show widget - let widget = createWidget("Coronavirus", `${res.todayCases} Today`, `${res.cases} Total`, "#000000") - Script.setWidget(widget) - Script.complete() -} else { - // make table - let table = new UITable() - - // add header - let row = new UITableRow() - row.isHeader = true - row.addText(`Coronavirus Stats in Saudi Arabia`) - table.addRow(row) - - // fill data - table.addRow(createRow("Cases", res.cases)) - table.addRow(createRow("Today", res.todayCases)) - table.addRow(createRow("Deaths", res.deaths)) - table.addRow(createRow("Recovered", res.recovered)) - table.addRow(createRow("Critical", res.critical)) - - if (config.runsWithSiri) - Speech.speak(`There are ${res.cases} cases in ${country}, and ${res.todayCases} cases today.`) - - // present table - table.present() -} - -function createRow(title, number) { - let row = new UITableRow() - row.addText(title) - row.addText(number.toString()).rightAligned() - return row -} - -function createWidget(pretitle, title, subtitle, color) { - let w = new ListWidget() -// w.backgroundColor = new Color(color) - let preTxt = w.addText(pretitle) -// preTxt.textColor = Color.white() - preTxt.textOpacity = 0.9 - preTxt.font = Font.systemFont(16) - w.addSpacer(5) - let titleTxt = w.addText(title) -// titleTxt.textColor = Color.white() - titleTxt.font = Font.systemFont(22) - w.addSpacer(5) - let subTxt = w.addText(subtitle) -// subTxt.textColor = Color.white() - subTxt.textOpacity = 0.9 - subTxt.font = Font.systemFont(18) - return w -} From 97496f5781d788feec02b195bf7d7c3803e70b10 Mon Sep 17 00:00:00 2001 From: d7eeem <35288751+d7eeem@users.noreply.github.com> Date: Tue, 27 Oct 2020 05:46:12 +0300 Subject: [PATCH 21/21] Add files via upload --- scripts/TermiWidget.js | 273 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 scripts/TermiWidget.js diff --git a/scripts/TermiWidget.js b/scripts/TermiWidget.js new file mode 100644 index 0000000..b3fb5ab --- /dev/null +++ b/scripts/TermiWidget.js @@ -0,0 +1,273 @@ +// Variables used by Scriptable. +// These must be at the very top of the file. Do not edit. +// icon-color: deep-gray; icon-glyph: magic; + +// Change these to your usernames! +const user = "evan"; + +// API PARAMETERS !important +// WEATHER_API_KEY, you need an Open Weather API Key +// You can get one for free at: https://home.openweathermap.org/api_keys (account needed). +const WEATHER_API_KEY = ""; +const DEFAULT_LOCATION = { + latitude: 0, + longitude: 0 +}; +const TAUTULLI_API_BASE = ""; +const TAUTULLI_API_KEY = ""; +const HOME_ASSISTANT_API_BASE = ""; +const HOME_ASSISTANT_API_KEY = ""; +const UPCOMING_SAT_PASS_URL = ""; + +const Cache = importModule('cache'); + +const cache = new Cache("termiWidgetCache"); +const data = await fetchData(); +const widget = createWidget(data); +Script.setWidget(widget); +Script.complete(); + +function createWidget(data) { + console.log(data) + const w = new ListWidget() + const bgColor = new LinearGradient() + bgColor.colors = [new Color("#29323c"), new Color("#1c1c1c")] + bgColor.locations = [0.0, 1.0] + w.backgroundGradient = bgColor + w.setPadding(12, 15, 15, 12) + + const stack = w.addStack(); + stack.layoutHorizontally(); + + const leftStack = stack.addStack(); + leftStack.layoutVertically(); + leftStack.spacing = 6; + leftStack.size = new Size(200, 0); + + const time = new Date() + const dfTime = new DateFormatter() + dfTime.locale = "en" + dfTime.useMediumDateStyle() + dfTime.useNoTimeStyle() + + const firstLine = leftStack.addText(`[] ${user} ~$ now`) + firstLine.textColor = Color.white() + firstLine.textOpacity = 0.7 + firstLine.font = new Font("Menlo", 11) + + const timeLine = leftStack.addText(`[🗓] ${dfTime.string(time)}`) + timeLine.textColor = Color.white() + timeLine.font = new Font("Menlo", 11) + + const batteryLine = leftStack.addText(`[🔋] ${renderBattery()}`) + batteryLine.textColor = new Color("#6ef2ae") + batteryLine.font = new Font("Menlo", 11) + + const locationLine = leftStack.addText(`[️️📍] Location: ${data.weather.location}`) + locationLine.textColor = new Color("#7dbbae") + locationLine.font = new Font("Menlo", 11) + + const homeLine = leftStack.addText(`[🏠] ${data.home.mode}, ${data.home.temperature}°, Lights ${data.home.lights ? "On" : "Off"}`); + homeLine.textColor = new Color("#ff9468") + homeLine.font = new Font("Menlo", 11) + + let plexText = `[🍿] Plex: ${data.plex.streams} stream${data.plex.streams == 1 ? '' : 's'}`; + // if (data.plex.streams > 0) { + // plexText += `, ${data.plex.transcodes} transcode${data.plex.transcodes == 1 ? '' : 's'}`; + // } + const plexLine = leftStack.addText(plexText); + plexLine.textColor = new Color("#ffa7d3") + plexLine.font = new Font("Menlo", 11) + + const satLine = leftStack.addText(`[🛰] ${data.satPass}`); + satLine.textColor = new Color("#ffcc66") + satLine.font = new Font("Menlo", 11) + + stack.addSpacer(); + const rightStack = stack.addStack(); + rightStack.spacing = 2; + rightStack.layoutVertically(); + rightStack.bottomAlignContent(); + + addWeatherLine(rightStack, data.weather.icon, 32); + addWeatherLine(rightStack, `${data.weather.description}, ${data.weather.temperature}°`, 12, true); + addWeatherLine(rightStack, `High: ${data.weather.high}°`); + addWeatherLine(rightStack, `Low: ${data.weather.low}°`); + addWeatherLine(rightStack, `Wind: ${data.weather.wind} mph`); + + return w +} + +function addWeatherLine(w, text, size, bold) { + const stack = w.addStack(); + stack.setPadding(0, 0, 0, 0); + stack.layoutHorizontally(); + stack.addSpacer(); + const line = stack.addText(text); + line.textColor = new Color("#ffcc66"); + line.font = new Font("Menlo" + (bold ? "-Bold" : ""), size || 11); +} + +async function fetchData() { + const weather = await fetchWeather(); + const plex = await fetchPlex(); + const home = await fetchHome(); + const satPass = await fetchNextSatPass(); + + return { + weather, + plex, + home, + satPass, + } +} + +function renderBattery() { + const batteryLevel = Device.batteryLevel() + const juice = "#".repeat(Math.floor(batteryLevel * 8)) + const used = ".".repeat(8 - juice.length) + const batteryAscii = `[${juice}${used}] ${Math.round(batteryLevel * 100)}%` + return batteryAscii +} + +async function fetchWeather() { + let location = await cache.read('location'); + if (!location) { + try { + Location.setAccuracyToThreeKilometers(); + location = await Location.current(); + } catch(error) { + location = await cache.read('location'); + } + } + if (!location) { + location = DEFAULT_LOCATION; + } + const address = await Location.reverseGeocode(location.latitude, location.longitude); + const url = "https://api.openweathermap.org/data/2.5/onecall?lat=" + location.latitude + "&lon=" + location.longitude + "&exclude=minutely,hourly,alerts&units=imperial&lang=en&appid=" + WEATHER_API_KEY; + const data = await fetchJson(`weather_${address[0].locality}`, url); + + return { + location: address[0].locality, + icon: getWeatherEmoji(data.weather[0].id, ((new Date()).getTime() / 1000) >= data.sys.sunset), + description: data.weather[0].main, + temperature: Math.round(data.main.temp), + wind: Math.round(data.wind.speed), + high: Math.round(data.main.temp_max), + low: Math.round(data.main.temp_min), + } +} + +async function fetchPlex() { + const url = `${TAUTULLI_API_BASE}/api/v2?apikey=${TAUTULLI_API_KEY}&cmd=get_activity`; + const data = await fetchJson(`plex`, url); + + return { + streams: data.response.data.stream_count, + transcodes: data.response.data.stream_count_transcode, + }; +} + +async function fetchHome() { + const mode = await fetchHomeAssistant('states/input_select.mode'); + const temp = await fetchHomeAssistant('states/sensor.hallway_temperature'); + const livingRoomLight = (await fetchHomeAssistant('states/light.living_room')).state == "on"; + const bedRoomLight = (await fetchHomeAssistant('states/light.bedroom')).state == "on"; + const hallwayLight = (await fetchHomeAssistant('states/light.hallway')).state == "on"; + const bathroomLight = (await fetchHomeAssistant('states/light.bathroom')).state == "on"; + + return { + mode: mode.state, + temperature: Math.round(parseFloat(temp.state)), + lights: livingRoomLight || bedRoomLight || hallwayLight || bathroomLight, + }; +} + +async function fetchHomeAssistant(path) { + return fetchJson(path, `${HOME_ASSISTANT_API_BASE}/api/${path}`, { + 'Authorization': `Bearer ${HOME_ASSISTANT_API_KEY}`, + 'Content-Type': 'application/json', + }); +} + +async function fetchNextSatPass() { + const passes = await fetchJson('upcoming-passes.json', UPCOMING_SAT_PASS_URL); + const now = new Date(); + const nextPass = passes + .filter((p) => now.getTime() < p.end)[0]; + + if (!nextPass) { + return 'No more passes today'; + } + + if (nextPass.start > now.getTime()) { + const minutes = Math.round(((nextPass.start - now.getTime()) / 1000) / 60); + const hours = Math.round((((nextPass.start - now.getTime()) / 1000) / 60) / 60); + + if (minutes > 59) { + return `${nextPass.satellite} in ${hours}h, ${Math.round(nextPass.elevation)}°`; + } else { + return `${nextPass.satellite} in ${minutes}m, ${Math.round(nextPass.elevation)}°`; + } + } else { + return `${nextPass.satellite} for ${Math.round(((nextPass.end - now.getTime()) / 1000) / 60)}m, ${Math.round(nextPass.elevation)}°`; + } +} + +async function fetchJson(key, url, headers) { + const cached = await cache.read(key, 5); + if (cached) { + return cached; + } + + try { + console.log(`Fetching url: ${url}`); + const req = new Request(url); + req.headers = headers; + const resp = await req.loadJSON(); + cache.write(key, resp); + return resp; + } catch (error) { + try { + return cache.read(key, 5); + } catch (error) { + console.log(`Couldn't fetch ${url}`); + } + } +} + +function getWeatherEmoji(code, isNight) { + if (code >= 200 && code < 300 || code == 960 || code == 961) { + return "⛈" + } else if ((code >= 300 && code < 600) || code == 701) { + return "🌧" + } else if (code >= 600 && code < 700) { + return "❄️" + } else if (code == 711) { + return "🔥" + } else if (code == 800) { + return isNight ? "🌕" : "☀️" + } else if (code == 801) { + return isNight ? "☁️" : "🌤" + } else if (code == 802) { + return isNight ? "☁️" : "⛅️" + } else if (code == 803) { + return isNight ? "☁️" : "🌥" + } else if (code == 804) { + return "☁️" + } else if (code == 900 || code == 962 || code == 781) { + return "🌪" + } else if (code >= 700 && code < 800) { + return "🌫" + } else if (code == 903) { + return "🥶" + } else if (code == 904) { + return "🥵" + } else if (code == 905 || code == 957) { + return "💨" + } else if (code == 906 || code == 958 || code == 959) { + return "🧊" + } else { + return "❓" + } +}