Weather Cal Code
Weather Cal Code
/*
This script contains the logic that allows Weather Cal to work. Please do not
modify this file. You can add customizations in the widget script.
Documentation is available at github.com/mzeryck/Weather-Cal
*/
const weatherCal = {
if (!this.fm.fileExists(this.fm.joinPath(this.fm.libraryDirectory(), "weather-
cal-setup"))) return await this.initialSetup(backgroundSettingExists)
if (backgroundSettingExists) return await this.editSettings(codeFilename,
gitHubUrl)
await this.generateAlert("Weather Cal is set up, but you need to choose a
background for this widget.",["Continue"])
return await this.setWidgetBackground()
},
message = (imported ? "Welcome to Weather Cal. We" : "Next, we") + " need to
check if you've given permissions to the Scriptable app. This might take a few
seconds."
await this.generateAlert(message,["Check permissions"])
let errors = []
if (!(await this.setupLocation())) { errors.push("location") }
try { await CalendarEvent.today() } catch { errors.push("calendar") }
try { await Reminder.all() } catch { errors.push("reminders") }
let issues
if (errors.length > 0) { issues = errors[0] }
if (errors.length == 2) { issues += " and " + errors[1] }
if (errors.length == 3) { issues += ", " + errors[1] + ", and " + errors[2] }
if (issues) {
message = "Scriptable does not have permission for " + issues + ". Some
features may not work without enabling them in the Settings app."
options = ["Continue setup anyway", "Exit setup"]
} else {
message = "Your permissions are enabled."
options = ["Continue setup"]
}
if (await this.generateAlert(message,options)) return
message = "To display the weather on your widget, you need an OpenWeather API
key."
options = ["I already have a key", "I need to get a key", "I don't want to show
weather info"]
const weather = await this.generateAlert(message,options)
message = "Your widget is ready! You'll now see a preview. Re-run this script
to edit the default preferences, including localization. When you're ready, add a
Scriptable widget to the home screen and select this script."
await this.generateAlert(message,["Show preview"])
return this.previewValue()
},
if (response == menu.update) {
if (await this.generateAlert("Would you like to update the Weather Cal code?
Your widgets will not be affected.",["Update", "Exit"])) return
const success = await this.downloadCode(codeFilename, gitHubUrl)
return await this.generateAlert(success ? "The update is now complete." :
"The update failed. Please try again later.")
}
if (response == menu.share) {
const layout =
this.fm.readString(this.fm.joinPath(this.fm.documentsDirectory(), this.name +
".js")).split('`')[1]
const prefs = JSON.stringify(await this.getSettings())
const bg = this.fm.readString(this.bgPath)
if (response == menu.other) {
const otherOptions = ["Re-enter API key", "Completely reset widget", "Exit"]
const otherResponse = await this.generateAlert("Other settings",otherOptions)
if ((await alert.present()) == 0) {
for (item of this.fm.listContents(this.fm.libraryDirectory())) {
if (item.startsWith("weather-cal-") && item != "weather-cal-api-key" &&
item != "weather-cal-setup") {
this.fm.remove(this.fm.joinPath(this.fm.libraryDirectory(), item))
}
}
const success = await this.downloadCode(this.name, this.widgetUrl)
const message = success ? "This script has been reset. Close the script
and reopen it for the change to take effect." : "The reset failed."
await this.generateAlert(message)
}
}
}
return
},
// Get the weather key, optionally determining if it's the first run.
async getWeatherKey(firstRun = false) {
const returnVal = await this.promptForText("Paste your API key in the box
below.",[""],["82c29fdbgd6aebbb595d402f8a65fabf"])
const apiKey = returnVal.textFieldValue(0)
if (!apiKey || apiKey == "" || apiKey == null) { return await
this.generateAlert("No API key was entered. Try copying the key again and re-
running this script.",["Exit"]) }
this.writePreference("weather-cal-api-key", apiKey)
const req = new Request("https://api.openweathermap.org/data/2.5/onecall?
lat=37.332280&lon=-122.010980&appid=" + apiKey)
try { val = await req.loadJSON() } catch { val = { current: false } }
if (!val.current) {
const message = firstRun ? "New OpenWeather API keys may take a few hours to
activate. Your widget will start displaying weather information once it's
active." : "The key you entered, " + apiKey + ", didn't work. If it's a new key, it
may take a few hours to activate."
await this.generateAlert(message,[firstRun ? "Continue" : "OK"])
} else if (backgroundType == 1) {
background.type = "auto"
} else if (backgroundType == 2) {
background.type = "gradient"
const returnVal = await this.promptForText("Gradient Colors",
[background.initialColor,background.finalColor,background.initialDark,background.fi
nalDark],["Top default color","Bottom default color","Top dark mode color","Bottom
dark mode color"],"Enter the hex values of the colors for your gradient. You can
optionally choose different background colors for dark mode.")
background.initialColor = returnVal.textFieldValue(0)
background.finalColor = returnVal.textFieldValue(1)
background.initialDark = returnVal.textFieldValue(2)
background.finalDark = returnVal.textFieldValue(3)
} else if (backgroundType == 3) {
background.type = "image"
let valText
if (Array.isArray(setting.val)) {
valText = setting.val.map(a => a.title).join(", ")
} else {
valText = setting.val + ""
}
if (returnVal) {
for (let i=0; i < keys.length; i++) {
setting.val[keys[i]] = prompt.textFieldValue(i).trim()
}
} else {
const capOptions =
[this.enum.caps.upper,this.enum.caps.lower,this.enum.caps.title,this.enum.caps.none
]
setting.val["caps"] = capOptions[await
this.generateAlert("Capitalization",capOptions)]
}
await this.loadPrefsTable(table,category)
}
if (item.color) {
const colorCell = row.addText(isSelected ? "\u25CF" : "\u25CB")
colorCell.titleColor = item.color
colorCell.widthWeight = 1
}
/*
* Widget spacing, background, and construction
* -------------------------------------------- */
} else {
this.settings = await this.getSettings()
this.settings.layout = layout
}
// Shared values.
this.locale = this.settings.widget.locale
this.padding = parseInt(this.settings.widget.padding)
this.localization = this.settings.localization
this.format = this.settings.font
this.custom = custom
this.darkMode = !(Color.dynamic(Color.white(),Color.black()).red)
// Widget setup.
this.widget = new ListWidget()
this.widget.spacing = 0
// Background setup.
const background = JSON.parse(this.fm.readString(this.bgPath))
gradient.colors = gradientSettings.color()
gradient.locations = gradientSettings.position()
this.widget.backgroundGradient = gradient
if (this.fm.fileExists(imagePath)) {
if (this.fm.isFileStoredIniCloud(imagePath)) { await
this.fm.downloadFileFromiCloud(imagePath) }
this.widget.backgroundImage = this.fm.readImage(imagePath)
} else if (config.runsInWidget) {
this.widget.backgroundColor = Color.gray()
} else {
this.generateAlert("Please choose a background image in the settings
menu.")
}
}
this.usingASCII = undefined
this.currentColumns = []
this.rowNeedsSetup = false
// If it's a line, enumerate previous columns (if any) and set up the new row.
if (line[0] == "-" && line[line.length-1] == "-") {
if (this.currentColumns.length > 0) {
for (col of this.currentColumns) {
if (!col) { continue }
this.column(this.currentColumn,col.width)
for (item of col.items) { await this.executeItem(item) }
}
this.currentColumns = []
}
return this.rowNeedsSetup = true
}
if (this.rowNeedsSetup) {
this.row(this.currentColumn)
this.rowNeedsSetup = false
}
if (rawItem.match(/\s+\d+\s+/)) {
const value = parseInt(trimmedItem)
if (value) { this.currentColumns[i].width = value }
continue
}
/*
* Data setup functions
* -------------------------------------------- */
} else {
calendars = calSetting
}
let numberOfDays = parseInt(eventSettings.numberOfDays)
numberOfDays = isNaN(numberOfDays) ? 1 : numberOfDays
}).slice(0,parseInt(eventSettings.numberOfEvents))
},
if (this.isNight(this.now)) {
return {
color() { return [new Color("16296b"), new Color("021033"), new
Color("021033"), new Color("113245")] },
position() { return [-0.5, 0.2, 0.5, 1] },
}
}
return {
color() { return [new Color("3a8cc1"), new Color("90c0df")] },
position() { return [0, 1] },
}
},
if (!locationCache || locationCache.cacheExpired) {
try { location = await Location.current() }
catch { location = locationCache || { cacheExpired: true } }
try {
const geocode = await Location.reverseGeocode(location.latitude,
location.longitude, this.locale)
location.locality = (geocode[0].locality || geocode[0].postalAddress.city)
|| geocode[0].administrativeArea
} catch {
location.locality = locationCache ? locationCache.locality : null
}
this.fm.writeString(sunPath, JSON.stringify(sunData))
} catch {}
}
this.data.sun = {}
this.data.sun.sunrise = sunData ? new Date(sunData.results.sunrise).getTime() :
null
this.data.sun.sunset = sunData ? new Date(sunData.results.sunset).getTime() :
null
this.data.sun.tomorrow = sunData ? new Date(sunData.results.tomorrow).getTime()
: null
},
if (!weatherData || weatherData.cacheExpired) {
try {
const apiKey =
this.fm.readString(this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-api-
key")).replace(/\"/g,"")
const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" +
this.data.location.latitude + "&lon=" + this.data.location.longitude +
"&exclude=minutely,alerts&units=" + this.settings.widget.units + "&lang=" + locale
+ "&appid=" + apiKey
weatherData = await new Request(weatherReq).loadJSON()
if (weatherData.cod) { weatherData = null }
if (weatherData) { this.fm.writeString(weatherPath,
JSON.stringify(weatherData)) }
} catch {}
}
this.data.weather = {}
this.data.weather.currentTemp = weatherData ? weatherData.current.temp : null
this.data.weather.currentCondition = weatherData ?
weatherData.current.weather[0].id : 100
this.data.weather.currentDescription = weatherData ? (english ?
weatherData.current.weather[0].main : weatherData.current.weather[0].description) :
"--"
this.data.weather.todayHigh = weatherData ? weatherData.daily[0].temp.max :
null
this.data.weather.todayLow = weatherData ? weatherData.daily[0].temp.min : null
this.data.weather.forecast = []
this.data.weather.hourly = []
for (let i=0; i <= 7; i++) {
this.data.weather.forecast[i] = weatherData ? ({High:
weatherData.daily[i].temp.max, Low: weatherData.daily[i].temp.min, Condition:
weatherData.daily[i].weather[0].id}) : { High: null, Low: null, Condition: 100 }
this.data.weather.hourly[i] = weatherData ? ({Temp:
weatherData.hourly[i].temp, Condition: weatherData.hourly[i].weather[0].id}) :
{ Temp: null, Condition: 100 }
}
if (!covidData || covidData.cacheExpired) {
try {
covidData = await new
Request("https://coronavirus-19-api.herokuapp.com/countries/" +
encodeURIComponent(this.settings.covid.country.trim())).loadJSON()
this.fm.writeString(covidPath, JSON.stringify(covidData))
} catch {}
}
this.data.covid = covidData || {}
},
if (!newsData || newsData.cacheExpired) {
try {
let rawData = await new Request(this.settings.news.url).loadString()
const isRss = rawData.includes("<rss")
rawData = getTag(rawData, isRss ? "item" : "entry",
parseInt(this.settings.news.numberOfItems))
if (!rawData || rawData.length == 0) { throw 0 }
newsData = []
for (item of rawData) {
const listing = {}
listing.title = scrubString(getTag(item, "title")[0])
listing.link = scrubString(getTag(item, "link")[0])
listing.date = new Date(getTag(item, isRss ? "pubDate" : "published")[0])
newsData.push(listing)
}
this.fm.writeString(newsPath, JSON.stringify(newsData))
} catch {}
}
this.data.news = newsData || []
/*
* Widget items
* -------------------------------------------- */
if (dateSettings.dynamicDateSize ? this.data.events.length :
dateSettings.staticDateSize == "small") {
this.provideText(this.formatDate(this.now,dateSettings.smallDateFormat),
column, this.format.smallDate, true)
} else {
const dateOneStack = this.align(column)
const dateOne =
this.provideText(this.formatDate(this.now,dateSettings.largeDateLineOne),
dateOneStack, this.format.largeDate1)
dateOneStack.setPadding(this.padding/2, this.padding, 0, this.padding)
if (this.data.events.length == 0) {
if (eventSettings.noEventBehavior == "message" &&
this.localization.noEventMessage.length) { return
this.provideText(this.localization.noEventMessage, column, this.format.noEvents,
true) }
if (this[eventSettings.noEventBehavior]) { return await
this[eventSettings.noEventBehavior](column) }
}
let currentStack
let currentDiff = 0
const numberOfEvents = this.data.events.length
const settingUrlExists = (eventSettings.url || "").length > 0
const showCalendarColor = eventSettings.showCalendarColor
const colorShape = showCalendarColor.includes("circle") ? "circle" :
"rectangle"
makeEventStack(currentDiff,this.now)
if (diff != currentDiff) {
currentDiff = diff
makeEventStack(currentDiff,this.now)
if (event.isAllDay) { continue }
if (this.data.reminders.length == 0) {
if (reminderSettings.noRemindersBehavior == "message" &&
this.localization.noRemindersMessage.length) { return
this.provideText(this.localization.noRemindersMessage, column,
this.format.noReminders, true) }
if (this[reminderSettings.noRemindersBehavior]) { return await
this[reminderSettings.noRemindersBehavior](column) }
}
let timeText
if (reminderSettings.useRelativeDueDate) {
const rdf = new RelativeDateTimeFormatter()
rdf.locale = this.locale
rdf.useNamedDateTimeStyle()
timeText = rdf.string(reminder.dueDate, this.now)
} else {
const df = new DateFormatter()
df.locale = this.locale
if (this.dateDiff(reminder.dueDate, this.now) == 0 &&
reminder.dueDateIncludesTime) { df.useNoDateStyle() }
else { df.useShortDateStyle() }
if (reminder.dueDateIncludesTime) { df.useShortTimeStyle() }
else { df.useNoTimeStyle() }
timeText = df.string(reminder.dueDate)
}
if (weatherSettings.showCondition) {
const conditionTextStack = this.align(currentWeatherStack)
this.provideText(weatherData.currentDescription, conditionTextStack,
this.format.smallTemp)
conditionTextStack.setPadding(this.padding, this.padding, 0, this.padding)
}
if (!weatherSettings.horizontalCondition) {
const tempStack = this.align(currentWeatherStack)
tempStack.setPadding(0, this.padding, 0, this.padding)
this.provideText(tempText, tempStack, this.format.largeTemp)
}
if (!weatherSettings.showHighLow) { return }
tempBarStack.addSpacer(1)
const subCondition =
subConditionStack.addImage(this.provideConditionSymbol(showNextHour ?
weatherData.hourly[1].Condition :
weatherData.forecast[1].Condition,nightCondition))
const subConditionSize = showNextHour ? 14 : 18
subCondition.imageSize = new Size(subConditionSize, subConditionSize)
this.tintIcon(subCondition, this.format.smallTemp)
subConditionStack.addSpacer(5)
if (showNextHour) {
this.provideText(this.displayNumber(weatherData.hourly[1].Temp,"--") + "°",
subConditionStack, this.format.smallTemp)
} else {
const tomorrowLine =
subConditionStack.addImage(this.drawVerticalLine(this.provideColor(this.format.tiny
Temp, 0.5), 20))
if (this.settings.widget.instantDark) this.tintIcon(tomorrowLine,
this.format.tinyTemp, true)
tomorrowLine.imageSize = new Size(3,28)
subConditionStack.addSpacer(5)
const tomorrowStack = subConditionStack.addStack()
tomorrowStack.layoutVertically()
this.provideText(this.displayNumber(weatherData.forecast[1].High,"-"),
tomorrowStack, this.format.tinyTemp)
tomorrowStack.addSpacer(4)
this.provideText(this.displayNumber(weatherData.forecast[1].Low,"-"),
tomorrowStack, this.format.tinyTemp)
}
if (weatherSettings.showRain) {
const subRainStack = this.align(futureWeatherStack)
subRainStack.layoutHorizontally()
subRainStack.centerAlignContent()
subRainStack.setPadding(0, this.padding, this.padding, this.padding)
if (horizontal) {
weatherStack.layoutHorizontally()
weatherStack.setPadding(this.padding, outsidePadding, this.padding,
outsidePadding)
} else {
weatherStack.layoutVertically()
weatherStack.setPadding(outsidePadding, this.padding, outsidePadding,
this.padding)
}
if (horizontal) {
unitStack.setPadding(0, initialSpace, 0, finalSpace)
unitStack.layoutVertically()
dateStack.addSpacer()
this.provideText(this.formatDate(myDate,dateFormat), dateStack,
this.format.smallTemp)
dateStack.addSpacer()
} else {
unitStack.setPadding(initialSpace, 0, finalSpace, 0)
unitStack.layoutHorizontally()
dateStack.layoutHorizontally()
dateStack.setPadding(0, 0, 0, 0)
dateStack.size = stackSize
unitStack.centerAlignContent()
unitStack.addSpacer(5)
if (horizontal) { conditionStack.addSpacer() }
unitStack.addSpacer(5)
if (horizontal) { tempStack.addSpacer() }
const temp =
this.provideText(this.displayNumber(weatherData.hourly[i].Temp,"--") + "°",
tempStack, this.format.smallTemp)
temp.lineLimit = 1
temp.minimumScaleFactor = 0.75
if (horizontal) {
temp.size = stackSize
tempStack.addSpacer()
}
} else {
const tinyFontSize = (this.format.tinyTemp && this.format.tinyTemp.size) ?
this.format.tinyTemp.size : this.format.defaultText.size
conditionStack.size = new Size(0,tinyFontSize*2.64)
const conditionIcon =
conditionStack.addImage(this.provideConditionSymbol(weatherData.forecast[i -
1].Condition, false))
conditionIcon.imageSize = new Size(18,18)
this.tintIcon(conditionIcon, this.format.smallTemp)
conditionStack.addSpacer(5)
const tempLine =
conditionStack.addImage(this.drawVerticalLine(this.provideColor(this.format.tinyTem
p, 0.5), 20))
if (this.settings.widget.instantDark) this.tintIcon(tempLine,
this.format.tinyTemp, true)
tempLine.imageSize = new Size(3,28)
conditionStack.addSpacer(5)
if (horizontal) { conditionStack.addSpacer() }
}
if (hourly) { myDate.setHours(myDate.getHours() + 1) }
}
},
sunriseStack.addSpacer(this.padding * 0.3)
sunriseStack.addSpacer(this.padding)
covidStack.addSpacer(this.padding * 0.3)
covidStack.addSpacer(this.padding)
const batteryIcon =
batteryStack.addImage(this.provideBatteryIcon(Device.batteryLevel(),Device.isChargi
ng()))
batteryIcon.imageSize = new Size(30,30)
batteryStack.addSpacer(this.padding * 0.6)
this.provideText(batteryLevel + "%", batteryStack, this.format.battery)
},
// Display a symbol.
symbol(column, name) {
if (!name || !SFSymbol.named(name)) { return }
if (!newsSettings.showDate) { continue }
/*
* Helper functions
* -------------------------------------------- */
// Returns the difference in days between two dates. Adapted from StackOverflow.
dateDiff(first, second) {
const firstDate = new Date(first.getFullYear(), first.getMonth(),
first.getDate(), 0, 0, 0)
const secondDate = new Date(second.getFullYear(), second.getMonth(),
second.getDate(), 0, 0, 0)
return Math.round((secondDate-firstDate)/(1000*60*60*24))
},
const batteryWidth = 87
const batteryHeight = 41
const x = batteryWidth*0.1525
const y = batteryHeight*0.247
const width = batteryWidth*0.602
const height = batteryHeight*0.505
case (capsEnum.lower):
return text.toLowerCase()
case (capsEnum.title):
return text.replace(/\w\S*/g,function(a) {
return a.charAt(0).toUpperCase() + a.substr(1).toLowerCase()
})
}
return text
}
return textItem
},
// Provide a color based on a format and the current dark mode state.
provideColor(format, alpha) {
const defaultText = this.format.defaultText
const lightColor = (format && format.color && format.color.length) ?
format.color : defaultText.color
const defaultDark = (defaultText.dark && defaultText.dark.length) ?
defaultText.dark : defaultText.color
const darkColor = (format && format.dark && format.dark.length) ? format.dark :
defaultDark
const width = 2
return draw.getImage()
},
draw.setFillColor(this.provideColor(this.format.tinyTemp, 0.5))
draw.fillPath()
return draw.getImage()
},
return settings
},
enum: {
caps: {
upper: "ALL CAPS",
lower: "all lowercase",
title: "Title Case",
none: "None (Default)",
},
icons: {
never: "Never",
always: "Always",
dark: "In dark mode",
light: "In light mode",
}
},
}
module.exports = weatherCal
/*
* Detect the current module
* by Raymond Velasquez @supermamon
* -------------------------------------------- */
/*
* Don't modify the characters below this line.
* -------------------------------------------- */
//4