Last active
January 9, 2021 01:15
-
-
Save PtruckStar/7f92f1409113054ecb2053949fb34ed1 to your computer and use it in GitHub Desktop.
modern ui scriptable app weather widget
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-gray; icon-glyph: cloud; | |
// made by @morinagapltynm | |
// credit to @ImGamez for weatherline | |
// credit to @mzeryck for SFSymbol code | |
const API_KEY = Keychain.get("WEATHER_ORG_API_KEY"); | |
const lockLocation = true; | |
const locale = "id"; | |
const nowstring = "now"; | |
const units = "metric"; | |
const twelveHours = false; | |
const roundedGraph = true; | |
const roundedTemp = true; | |
const hoursToShow = 4; | |
const spaceBetweenDays = 70; | |
const iconSize = 34; | |
const accentColor = new Color("#EB6E4E", 1); | |
const forceImageUpdate = false; | |
//preparing element | |
const files = FileManager.local(); | |
const currentDate = new Date(); | |
const dateFormatter = new DateFormatter(); | |
dateFormatter.locale = locale; | |
const drawContext = new DrawContext(); | |
drawContext.size = new Size(665, 220); | |
drawContext.opaque = false; | |
drawContext.respectScreenScale = true; | |
drawContext.setTextAlignedCenter(); | |
let usingCachedData; | |
let readLocationFromFile; | |
let loc = await setupLocation(); | |
//constracting widget | |
let widget = new ListWidget(); | |
widget.setPadding(0, 0, 0, 0); | |
background(); | |
let weatherStack = widget.addStack(); | |
weatherStack.backgroundColor = new Color("242424"); | |
weatherStack.addImage(await weather()); | |
widget.presentLarge(); | |
//========== | |
// element | |
//========== | |
async function weather() { | |
let weatherData = await fetchData( | |
"https://api.openweathermap.org/data/2.5/onecall?lat=" + loc.latitude + "&lon=" + loc.longitude + "&exclude=minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + API_KEY, | |
"weather-cal-cache" | |
); | |
let maxHeight = 140; | |
let weatherY = 120; | |
let min, max, diff; | |
for (let i = 0; i <= hoursToShow; i++) { | |
let temp = shouldRound(roundedGraph, weatherData.hourly[i + 1].temp); | |
min = temp < min || min == undefined ? temp : min; | |
max = temp > max || max == undefined ? temp : max; | |
} | |
diff = max - min; | |
//current weather column | |
drawContext.setFillColor(new Color("303030")); | |
drawContext.fillRect(new Rect(15, 0, 85, 220)); | |
drawTextBox(shouldRound(roundedTemp, weatherData.current.temp) + "°", Font.boldSystemFont(30), 30, 20, 60, 30, Color.white()); | |
drawImage(symbolForCondition(weatherData.current.weather[0].id, false), 35, 60); | |
drawTextBox(nowstring, Font.boldSystemFont(18), 30, 180, 50, 21, Color.gray()); | |
//sunset sunrise | |
const sunrise = new Date(weatherData.current.sunrise * 1000); | |
const sunset = new Date(weatherData.current.sunset * 1000); | |
const sunPos = [10, 120]; //x, y | |
const isSunrise = currentDate > sunrise && currentDate < sunset ? false : true; | |
drawImage(isSunrise ? symbolForCondition(901, night) : symbolForCondition(902, night), sunPos[0] + 30, sunPos[1] + 25); //sunrise 901 : sunset 902 | |
drawTextBox(formatTime(new Date(isSunrise ? sunrise : sunset)), new Font("Futura", 20), sunPos[0] + 6, sunPos[1], 80, 50, Color.white()); | |
//offline mode | |
if(usingCachedData) offLineMode(); | |
//weather line column | |
for (let i = 0; i <= hoursToShow; i++) { | |
let hourData = weatherData.hourly[i + 1]; | |
let nextHourTemp = shouldRound(roundedGraph, weatherData.hourly[i + 2].temp); | |
let hour = epochToDate(hourData.dt).getHours(); | |
if (twelveHours) hour = hour > 12 ? hour - 12 : hour == 0 ? "12a" : hour == 12 ? "12p" : hour; | |
let 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) { | |
var hourDay = epochToDate(hourData.dt); | |
for (let i2 = 0; i2 < weatherData.daily.length; i2++) { | |
let day = weatherData.daily[i2]; | |
if (isSameDay(epochToDate(day.dt), epochToDate(hourData.dt))) { | |
hourDay = day; | |
break; | |
} | |
} | |
// 'Night' boolean for line graph and SFSymbols | |
var night = hourData.dt > hourDay.sunset || hourData.dt < hourDay.sunrise; | |
drawLine(spaceBetweenDays * i + weatherY + 20, maxHeight - 50 * delta, spaceBetweenDays * (i + 1) + weatherY + 20, maxHeight - 50 * nextDelta, 4, night ? Color.gray() : accentColor); | |
} | |
let lastY = maxHeight + 35; | |
for (o = 0; o < 100; o++) { | |
drawLine(spaceBetweenDays * i + weatherY + 20, o == 0 ? maxHeight + 35 : lastY, spaceBetweenDays * i + weatherY + 20, lastY - 5, 2, Color.gray()); | |
lastY -= 10; | |
if (lastY <= maxHeight - 50 * delta) break; | |
} | |
drawTextBox(shouldRound(roundedTemp, temp) + "°", Font.boldSystemFont(18), spaceBetweenDays * i + weatherY, maxHeight - 50 - 50 * delta, 50, 21, Color.white()); | |
// Next 2 lines SFSymbols tweak | |
const condition = hourData.weather[0].id; | |
drawImage(symbolForCondition(condition, night), spaceBetweenDays * i + weatherY, maxHeight - 20 - 50 * delta); | |
drawTextBox(hour, Font.boldSystemFont(18), spaceBetweenDays * i + weatherY - 5, maxHeight + 40, 50, 21, Color.gray()); | |
previousDelta = delta; | |
} | |
//right column | |
drawContext.setFillColor(new Color("C1BEBE")); | |
drawContext.fillRect(new Rect(465, 0, 200, 220)); | |
drawTextBox(currentDate.getDate(), new Font("Futura", 90), 465, 40, 200, 130, new Color("242424")); | |
drawTextBox(days().toUpperCase(), new Font("Futura", 20), 465, 150, 200, 130, new Color("242424")); | |
return drawContext.getImage(); | |
} | |
//======================== | |
//=====data fetcher======= | |
//======================== | |
async function background() { | |
const path = files.joinPath(files.documentsDirectory(), "weather-cal-image-eric"); | |
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); | |
} | |
} | |
async function setupLocation() { | |
let locationData = {}; | |
const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc"); | |
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]; | |
} | |
return locationData; | |
} | |
async function fetchData(url, fileName) { | |
const cachePath = files.joinPath(files.documentsDirectory(), fileName); | |
const cacheExists = files.fileExists(cachePath); | |
const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0; | |
var data; | |
if (cacheExists && currentDate.getTime() - cacheDate.getTime() < 60000) { | |
let raw = files.readString(cachePath); | |
data = JSON.parse(raw); | |
} else { | |
try { | |
data = await new Request(url).loadJSON(); | |
files.writeString(cachePath, JSON.stringify(data)); | |
} catch (e) { | |
console.log("Offline mode"); | |
try { | |
raw = files.readString(cachePath); | |
data = JSON.parse(raw); | |
usingCachedData = true; | |
} catch (e2) { | |
console.log("Error: No offline data cached"); | |
} | |
} | |
} | |
return data; | |
} | |
//======================== | |
//=========helper========= | |
//======================== | |
function offLineMode() { | |
drawContext.setFillColor(new Color("303030", 0.5)); | |
drawContext.fillRect(new Rect(15, 0, 85, 220)); | |
drawContext.setFillColor(Color.white()) | |
drawTextBox("⚠️", new Font("Futura", 50), 15, 65, 85, 80, Color.white()) | |
drawTextBox("offline", Font.systemFont(20), 15, 125, 85, 80, Color.white()) | |
} | |
function epochToDate(epoch) { | |
return new Date(epoch * 1000); | |
} | |
function formatTime(date) { | |
dateFormatter.useNoDateStyle(); | |
dateFormatter.useShortTimeStyle(); | |
return dateFormatter.string(date); | |
} | |
function days() { | |
dateFormatter.dateFormat = "EEEE"; | |
return dateFormatter.string(currentDate); | |
} | |
function months() { | |
dateFormatter.dateFormat = "MMMM"; | |
return dateFormatter.string(currentDate); | |
} | |
function drawImage(image, x, y) { | |
drawContext.drawImageAtPoint(image, new Point(x, y)); | |
} | |
function drawTextBox(text, font, x, y, w, h, color = Color.black()) { | |
drawContext.setFont(font); | |
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; | |
} | |
function isSameDay(date1, date2) { | |
return date1.getYear() == date2.getYear() && date1.getMonth() == date2.getMonth() && date1.getDate() == date2.getDate(); | |
} | |
// SFSymbol function | |
function symbolForCondition(cond, night) { | |
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"; | |
}, | |
"9": function () { | |
return cond == 902 ? "sunset.fill" : "sunrise.fill"; | |
} | |
}; | |
// Get first condition digit. | |
let conditionDigit = Math.floor(cond / 100); | |
// Style and return the symbol. | |
let sfs = SFSymbol.named(symbols[conditionDigit]()); | |
sfs.applyFont(Font.systemFont(conditionDigit == 9 ? 20 : iconSize)); | |
return sfs.image; | |
} | |
Script.complete(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment