Created
October 5, 2020 18:09
-
-
Save GeoffreyDMartin/8c7b8325ca762e7620494c02718e449f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: deep-purple; icon-glyph: image; | |
// This widget was complied by Geoffrey Martin (@GeoffreyDMartin) and uses multiple ideas and scripts from the Scriptable Forums at talk.automators.com. | |
// The "transparency" portion of the script from using code from a widget created by Max Zeryck @mzeryck (see https://gist.github.com/mzeryck/3a97ccd1e059b3afa3c6666d27a496c9) | |
// The weather portion of the script was taken from ImGamez (see https://gist.github.com/ImGamez/a8f9d77bf660d7703cc96fee87cdc4b0) | |
/* | |
* Change the widget settings and test out the widget in this section. | |
* =================================================================== | |
*/ | |
/* -- RESET YOUR WIDGET -- */ | |
// Change to true to reset the widget background. | |
const resetWidget = false | |
/* -- PREVIEW YOUR WIDGET -- */ | |
// Change to true to see a preview of your widget. | |
const testMode = true | |
// Optionally specify the size of your widget preview. | |
const widgetPreview = "large" | |
/* -- FONTS AND TEXT -- */ | |
// Use iosfonts.com, or change to "" for the system font. | |
const fontName = "Futura-Medium" | |
// Find colors on htmlcolorcodes.com | |
const fontColor = new Color("#ffffff") | |
// Change the font sizes for each element. | |
const dayOfWeekSize = 60 | |
const todaySize = 18 | |
const activeTimerHeaderSize = 14 | |
const activeTimerSize = 36 | |
const untilSize = 14 | |
/* -- WEATHER API -- */ | |
const widgetParams = JSON.parse((args.widgetParameter != null) ? args.widgetParameter : '{ "LAT" : "33.9137" , "LON" : "-98.4943" , "LOC_NAME" : "Wichita Falls, TX" }') | |
const API_KEY = "REPLACE WITH YOUR OPENWEATHERMAP APIKEY" | |
const LAT = widgetParams.LAT | |
const LON = widgetParams.LON | |
const LOCATION_NAME = widgetParams.LOC_NAME | |
const units = "imperial" | |
const roundedGraph = true | |
const roundedTemp = true | |
const hoursToShow = (config.widgetFamily == "small") ? 3 : 11; | |
const spaceBetweenDays = (config.widgetFamily == "small") ? 60 : 44; | |
// Widget Size !important. | |
// Since the widget works "making" an image and displaying it as the widget background, you need to specify the exact size of the widget to | |
// get an 1:1 display ratio, if you specify an smaller size than the widget itself it will be displayed blurry. | |
// You can get the size simply taking an screenshot of your widgets on the home screen and measuring them in an image-proccessing software. | |
// contextSize : number > Height of the widget in screen pixels, this depends on you screen size (for an 4 inch display the small widget is 282 * 282 pixels on the home screen) | |
const contextSize = 222 | |
// mediumWidgetWidth : number > Width of the medium widget in pixels, this depends on you screen size (for an 4 inch display the medium widget is 584 pixels long on the home screen) | |
const mediumWidgetWidth = 584 | |
// accentColor : Color > Accent color of some elements (Graph lines and the location label). | |
const accentColor = new Color("#EB6E4E", 1) | |
// backgroundColor : Color > Background color of the widgets. | |
const backgroundColor = new Color("#1C1C1E", 0.2) | |
// Position and size of the elements on the widget. | |
// All coordinates make reference to the top-left of the element. | |
// locationNameCoords : Point > Define the position in pixels of the location label. | |
const locationNameCoords = new Point(30, 20) | |
// locationNameFontSize : number > Size in pixels of the font of the location label. | |
const locationNameFontSize = 36 | |
// locationNameCoords : Point > Define the position in pixels of the location label. | |
const tempCoords = new Rect((config.widgetFamily == "small") ? 150 : 450, 20, 100, 36) | |
// locationNameFontSize : number > Size in pixels of the font of the location label. | |
const tempFontSize = 36 | |
// weatherDescriptionCoords : Point > Position of the weather description label in pixels. | |
const weatherDescriptionCoords = new Rect((config.widgetFamily == "small") ? 150 : 450, 62, 100, 36) | |
// weatherDescriptionFontSize : number > Font size of the weather description label. | |
const weatherDescriptionFontSize = 18 | |
//footerFontSize : number > Font size of the footer labels (feels like... and last update time). | |
const footerFontSize = 20 | |
//feelsLikeCoords : Point > Coordinates of the "feels like" label. | |
const feelsLikeCoords = new Point(30, 230) | |
//lastUpdateTimePosAndSize : Rect > Defines the coordinates and size of the last updated time label. | |
const lastUpdateTimePosAndSize = new Rect((config.widgetFamily == "small") ? 150 : 450, 230, 100, footerFontSize + 1) | |
// Prepare for the SFSymbol request by getting sunset/sunrise times. | |
const date = new Date() | |
const sunData = await new Request("https://api.sunrise-sunset.org/json?lat=" + LAT + "&lng=" + LON + "&formatted=0&date=" + date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()).loadJSON(); | |
//From here proceed with caution. | |
let fm = FileManager.iCloud(); | |
let cachePath = fm.joinPath(fm.documentsDirectory(), "weatherCache"); | |
if (!fm.fileExists(cachePath)) { | |
fm.createDirectory(cachePath) | |
} | |
let weatherData; | |
let usingCachedData = false; | |
let drawContext = new DrawContext(); | |
drawContext.size = new Size((config.widgetFamily == "small") ? contextSize : mediumWidgetWidth, contextSize) | |
drawContext.opaque = false | |
drawContext.setTextAlignedCenter() | |
try { | |
weatherData = await new Request("https://api.openweathermap.org/data/2.5/onecall?lat=" + LAT + "&lon=" + LON + "&exclude=daily,minutely,alerts&units=" + units + "&lang=en&appid=" + API_KEY).loadJSON(); | |
fm.writeString(fm.joinPath(cachePath, "lastread"), JSON.stringify(weatherData)); | |
} catch (e) { | |
console.log("Offline mode") | |
try { | |
let raw = fm.readString(fm.joinPath(cachePath, "lastread")); | |
weatherData = JSON.parse(raw); | |
usingCachedData = true; | |
} catch (e2) { | |
console.log("Error: No offline data cached") | |
} | |
} | |
/* -- END WEATHER PROPERITIES -- */ | |
/* | |
* The code below this comment is the widget logic - a bit more complex. | |
* ===================================================================== | |
*/ | |
/* -- GLOBAL VALUES -- */ | |
// Widgets are unique based on the name of the script. | |
const filename = Script.name() + ".jpg" | |
const files = FileManager.local() | |
const path = files.joinPath(files.documentsDirectory(), filename) | |
const fileExists = files.fileExists(path) | |
// Store other global values. | |
let widget = new ListWidget() | |
// If we're in the widget or testing, build the widget. | |
if (config.runsInWidget || (testMode && fileExists && !resetWidget)) { | |
widget.setPadding(0, 0, 0, 0); | |
widget.backgroundImage = files.readImage(path) | |
/* -- ENTER YOUR CUSTOM WIDGET UI HERE -- */ | |
let formatter = new DateFormatter() | |
formatter.dateFormat = "EEEE" | |
let dayOfWeekString = formatter.string(new Date()).toUpperCase() | |
let dayOfWeekText = widget.addText(dayOfWeekString) | |
dayOfWeekText.textColor = fontColor | |
dayOfWeekText.font = new Font(fontName, dayOfWeekSize) | |
dayOfWeekText.lineLimit = 1 | |
dayOfWeekText.minimumScaleFactor = 0.2 | |
dayOfWeekText.centerAlignText() | |
formatter.dateFormat = "MMMM d, yyyy" | |
let dateString = formatter.string(new Date()) | |
let dateText = widget.addText(dateString) | |
dateText.textColor = fontColor | |
dateText.font = new Font(fontName, todaySize) | |
dateText.lineLimit = 1 | |
dateText.minimumScaleFactor = 0.2 | |
dateText.centerAlignText() | |
widget.addSpacer(25) | |
/* -- WEATHER PORTION UI by ImGamez -- */ | |
drawText(LOCATION_NAME, locationNameFontSize, locationNameCoords.x, locationNameCoords.y, Color.white()); | |
drawContext.setTextAlignedRight(); | |
drawTextC(Math.round(weatherData.current.temp) + "°", tempFontSize, tempCoords.x, tempCoords.y, tempCoords.width, tempCoords.height, Color.white()); | |
drawTextC(weatherData.current.weather[0].description, weatherDescriptionFontSize, weatherDescriptionCoords.x, weatherDescriptionCoords.y, weatherDescriptionCoords.width, weatherDescriptionCoords.height, Color.white()) | |
drawContext.setTextAlignedRight(); | |
let min, max, diff; | |
for (let i = 0; i <= hoursToShow; i++) { | |
let temp = shouldRound(roundedGraph, weatherData.hourly[i].temp); | |
min = (temp < min || min == undefined ? temp : min) | |
max = (temp > max || max == undefined ? temp : max) | |
} | |
diff = max - min; | |
for (let i = 0; i <= hoursToShow; i++) { | |
let hourData = weatherData.hourly[i]; | |
let nextHourTemp = shouldRound(roundedGraph, weatherData.hourly[i + 1].temp); | |
let hour = epochToDate(hourData.dt).getHours(); | |
hour = (hour > 12 ? hour - 12 : (hour == 0 ? "12a" : ((hour == 12) ? "12p" : hour))) | |
let temp = i == 0 ? weatherData.current.temp : hourData.temp | |
let delta = (diff > 0) ? (shouldRound(roundedGraph, temp) - min) / diff : 0.5; | |
let nextDelta = (diff > 0) ? (nextHourTemp - min) / diff : 0.5 | |
if (i < hoursToShow) | |
drawLine(spaceBetweenDays * (i) + 50, 175 - (50 * delta), spaceBetweenDays * (i + 1) + 50, 175 - (50 * nextDelta), 4, (hourData.dt > weatherData.current.sunset ? Color.gray() : accentColor)) | |
drawContext.setTextAlignedCenter(); | |
drawTextC(shouldRound(roundedTemp, temp) + "°", 20, spaceBetweenDays * i + 30, 135 - (50 * delta), 50, 21, Color.white()) | |
// The next three lines were modified for SFSymbol support. | |
const condition = i == 0 ? weatherData.current.weather[0].id : hourData.weather[0].id | |
const condDate = i == 0 ? weatherData.current.dt : hourData.dt | |
drawImage(symbolForCondition(condition, condDate), spaceBetweenDays * i + 40, 165 - (50 * delta)); | |
drawContext.setTextAlignedCenter(); | |
drawTextC((i == 0 ? "Now" : hour), 18, spaceBetweenDays * i + 30, 200, 50, 21, Color.gray()) | |
drawContext.setTextAlignedLeft(); | |
previousDelta = delta; | |
} | |
let weatherUI = widget.addImage(drawContext.getImage()) | |
weatherUI.centerAlignImage() | |
widget.addSpacer(25) | |
/* -- MORE CUSTOM UI UNDER THE WEATHER UI -- */ | |
let moreText = widget.addText("This is more text") | |
moreText.font = new Font(fontName, 18) | |
moreText.textColor = fontColor | |
moreText.centerAlignText() | |
/* -- End CUSTOM UI CODE-- */ | |
Script.setWidget(widget) | |
// This previews the widget in Scriptable | |
if (testMode) { | |
let widgetSizeFormat = widgetPreview.toLowerCase() | |
if (widgetSizeFormat == "small") { | |
widget.presentSmall() | |
} | |
if (widgetSizeFormat == "medium") { | |
widget.presentMedium() | |
} | |
if (widgetSizeFormat == "large") { | |
widget.presentLarge() | |
} | |
} | |
Script.complete() | |
// If we're running normally, go to the calendar. | |
} else if (fileExists && !resetWidget) { | |
const appleDate = new Date('2001/01/01') | |
const timestamp = (date.getTime() - appleDate.getTime()) / 1000 | |
const callback = new CallbackURL("calshow:" + timestamp) | |
callback.open() | |
Script.complete() | |
// If it's the first time it's running, set up the widget background. | |
} else { | |
// Determine if user has taken the screenshot. | |
var message | |
message = "Before you start, go to your home screen and enter wiggle mode. Scroll to the empty page on the far right and take a screenshot." | |
let exitOptions = ["Continue", "Exit to Take Screenshot"] | |
let shouldExit = await generateAlert(message, exitOptions) | |
if (shouldExit) return | |
// Get screenshot and determine phone size. | |
let img = await Photos.fromLibrary() | |
let height = img.size.height | |
let phone = phoneSizes()[height] | |
if (!phone) { | |
message = "It looks like you selected an image that isn't an iPhone screenshot, or your iPhone is not supported. Try again with a different image." | |
await generateAlert(message, ["OK"]) | |
return | |
} | |
// Prompt for widget size and position. | |
message = "What size of widget are you creating?" | |
let sizes = ["Small", "Medium", "Large"] | |
let size = await generateAlert(message, sizes) | |
let widgetSize = sizes[size] | |
message = "What position will it be in?" | |
message += (height == 1136 ? " (Note that your device only supports two rows of widgets, so the middle and bottom options are the same.)" : "") | |
// Determine image crop based on phone size. | |
let crop = { | |
w: "", | |
h: "", | |
x: "", | |
y: "" | |
} | |
if (widgetSize == "Small") { | |
crop.w = phone.small | |
crop.h = phone.small | |
let positions = ["Top left", "Top right", "Middle left", "Middle right", "Bottom left", "Bottom right"] | |
let position = await generateAlert(message, positions) | |
// Convert the two words into two keys for the phone size dictionary. | |
let keys = positions[position].toLowerCase().split(' ') | |
crop.y = phone[keys[0]] | |
crop.x = phone[keys[1]] | |
} else if (widgetSize == "Medium") { | |
crop.w = phone.medium | |
crop.h = phone.small | |
// Medium and large widgets have a fixed x-value. | |
crop.x = phone.left | |
let positions = ["Top", "Middle", "Bottom"] | |
let position = await generateAlert(message, positions) | |
let key = positions[position].toLowerCase() | |
crop.y = phone[key] | |
} else if (widgetSize == "Large") { | |
crop.w = phone.medium | |
crop.h = phone.large | |
crop.x = phone.left | |
let positions = ["Top", "Bottom"] | |
let position = await generateAlert(message, positions) | |
// Large widgets at the bottom have the "middle" y-value. | |
crop.y = position ? phone.middle : phone.top | |
} | |
// Crop image and finalize the widget. | |
let imgCrop = cropImage(img, new Rect(crop.x, crop.y, crop.w, crop.h)) | |
files.writeImage(path, imgCrop) | |
message = "Your widget background is ready. If you haven't already granted Calendar access, it will pop up next." | |
await generateAlert(message, ["OK"]) | |
// Make sure we have calendar access. | |
await CalendarEvent.today([]) | |
Script.complete() | |
} | |
/* | |
* Helper functions | |
* ================ | |
*/ | |
async function loadImage(imgName) { | |
if (fm.fileExists(fm.joinPath(cachePath, imgName))) { | |
return Image.fromData(Data.fromFile(fm.joinPath(cachePath, imgName))) | |
} else { | |
let imgdata = await new Request("https://openweathermap.org/img/wn/" + imgName + ".png").load(); | |
let img = Image.fromData(imgdata); | |
fm.write(fm.joinPath(cachePath, imgName), imgdata); | |
return img; | |
} | |
} | |
function epochToDate(epoch) { | |
return new Date(epoch * 1000) | |
} | |
function drawText(text, fontSize, x, y, color = Color.black()) { | |
drawContext.setFont(Font.boldSystemFont(fontSize)) | |
drawContext.setTextColor(color) | |
drawContext.drawText(new String(text).toString(), new Point(x, y)) | |
} | |
function drawImage(image, x, y) { | |
drawContext.drawImageAtPoint(image, new Point(x, y)) | |
} | |
function drawTextC(text, fontSize, x, y, w, h, color = Color.black()) { | |
drawContext.setFont(Font.boldSystemFont(fontSize)) | |
drawContext.setTextColor(color) | |
drawContext.drawTextInRect(new String(text).toString(), new Rect(x, y, w, h)) | |
} | |
function drawLine(x1, y1, x2, y2, width, color) { | |
const path = new Path() | |
path.move(new Point(x1, y1)) | |
path.addLine(new Point(x2, y2)) | |
drawContext.addPath(path) | |
drawContext.setStrokeColor(color) | |
drawContext.setLineWidth(width) | |
drawContext.strokePath() | |
} | |
function shouldRound(should, value) { | |
return ((should) ? Math.round(value) : value) | |
} | |
// This function returns an SFSymbol image for a weather condition. | |
function symbolForCondition(cond, condDate) { | |
const sunrise = new Date(sunData.results.sunrise).getTime() | |
const sunset = new Date(sunData.results.sunset).getTime() | |
const timeValue = condDate * 1000 | |
// Is it night at the provided date? | |
const night = (timeValue < sunrise) || (timeValue > sunset) | |
// 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) { | |
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. | |
let condImage = SFSymbol.named(symbols[conditionDigit]()) | |
condImage.applyFont(Font.title2()) | |
return condImage.image | |
} | |
// Generate an alert with the provided array of options. | |
async function generateAlert(message, options) { | |
let alert = new Alert() | |
alert.message = message | |
for (const option of options) { | |
alert.addAction(option) | |
} | |
let response = await alert.presentAlert() | |
return response | |
} | |
// Crop an image into the specified rect. | |
function cropImage(img, rect) { | |
let draw = new DrawContext() | |
draw.size = new Size(rect.width, rect.height) | |
draw.drawImageAtPoint(img, new Point(-rect.x, -rect.y)) | |
return draw.getImage() | |
} | |
// Pixel sizes and positions for widgets on all supported phones. | |
function phoneSizes() { | |
let phones = { | |
"2688": { | |
"small": 507, | |
"medium": 1080, | |
"large": 1137, | |
"left": 81, | |
"right": 654, | |
"top": 228, | |
"middle": 858, | |
"bottom": 1488 | |
}, | |
"1792": { | |
"small": 338, | |
"medium": 720, | |
"large": 758, | |
"left": 54, | |
"right": 436, | |
"top": 160, | |
"middle": 580, | |
"bottom": 1000 | |
}, | |
"2436": { | |
"small": 465, | |
"medium": 987, | |
"large": 1035, | |
"left": 69, | |
"right": 591, | |
"top": 213, | |
"middle": 783, | |
"bottom": 1353 | |
}, | |
"2208": { | |
"small": 471, | |
"medium": 1044, | |
"large": 1071, | |
"left": 99, | |
"right": 672, | |
"top": 114, | |
"middle": 696, | |
"bottom": 1278 | |
}, | |
"1334": { | |
"small": 296, | |
"medium": 642, | |
"large": 648, | |
"left": 54, | |
"right": 400, | |
"top": 60, | |
"middle": 412, | |
"bottom": 764 | |
}, | |
"1136": { | |
"small": 282, | |
"medium": 584, | |
"large": 622, | |
"left": 30, | |
"right": 332, | |
"top": 59, | |
"middle": 399, | |
"bottom": 399 | |
}, | |
"1624": { | |
"small": 310, | |
"medium": 658, | |
"large": 690, | |
"left": 46, | |
"right": 394, | |
"top": 142, | |
"middle": 522, | |
"bottom": 902 | |
} | |
} | |
return phones | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
this is EXACTLY what I was looking for! Thank you so much! ''
One question - How do I add calendar events below the weather line piece like in your screenshot?