Skip to content

Instantly share code, notes, and snippets.

@PtruckStar
Last active January 9, 2021 01:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PtruckStar/7f92f1409113054ecb2053949fb34ed1 to your computer and use it in GitHub Desktop.
Save PtruckStar/7f92f1409113054ecb2053949fb34ed1 to your computer and use it in GitHub Desktop.
modern ui scriptable app weather widget
// 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