Guest User

Weather cal code

a guest
Feb 9th, 2022
104
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 99.64 KB | None | 0 0
  1.  
  2. /*
  3.  
  4. 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.
  5. Documentation is available at github.com/mzeryck/Weather-Cal
  6.  
  7. */
  8.  
  9. const weatherCal = {
  10.  
  11. // Initialize shared properties.
  12. initialize(name, iCloudInUse) {
  13. this.name = name
  14. this.fm = iCloudInUse ? FileManager.iCloud() : FileManager.local()
  15. this.bgPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-" + this.name)
  16. this.prefPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-preferences-" + name)
  17. this.widgetUrl = "https://raw.githubusercontent.com/mzeryck/Weather-Cal/main/weather-cal.js"
  18. this.now = new Date()
  19. this.data = {}
  20. this.initialized = true
  21. },
  22.  
  23. // Determine what to do when Weather Cal is run.
  24. async runSetup(name, iCloudInUse, codeFilename, gitHubUrl) {
  25. if (!this.initialized) this.initialize(name, iCloudInUse)
  26. const backgroundSettingExists = this.fm.fileExists(this.bgPath)
  27.  
  28. if (!this.fm.fileExists(this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-setup"))) return await this.initialSetup(backgroundSettingExists)
  29. if (backgroundSettingExists) return await this.editSettings(codeFilename, gitHubUrl)
  30. await this.generateAlert("Weather Cal is set up, but you need to choose a background for this widget.",["Continue"])
  31. return await this.setWidgetBackground()
  32. },
  33.  
  34. // Run the initial setup.
  35. async initialSetup(imported = false) {
  36. let message, options
  37. if (!imported) {
  38. message = "Welcome to Weather Cal. Make sure your script has the name you want before you begin."
  39. options = ['I like the name "' + this.name + '"', "Let me go change it"]
  40. if (await this.generateAlert(message,options)) return
  41. }
  42.  
  43. 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."
  44. await this.generateAlert(message,["Check permissions"])
  45.  
  46. let errors = []
  47. if (!(await this.setupLocation())) { errors.push("location") }
  48. try { await CalendarEvent.today() } catch { errors.push("calendar") }
  49. try { await Reminder.all() } catch { errors.push("reminders") }
  50.  
  51. let issues
  52. if (errors.length > 0) { issues = errors[0] }
  53. if (errors.length == 2) { issues += " and " + errors[1] }
  54. if (errors.length == 3) { issues += ", " + errors[1] + ", and " + errors[2] }
  55.  
  56. if (issues) {
  57. message = "Scriptable does not have permission for " + issues + ". Some features may not work without enabling them in the Settings app."
  58. options = ["Continue setup anyway", "Exit setup"]
  59. } else {
  60. message = "Your permissions are enabled."
  61. options = ["Continue setup"]
  62. }
  63. if (await this.generateAlert(message,options)) return
  64.  
  65. message = "To display the weather on your widget, you need an OpenWeather API key."
  66. options = ["I already have a key", "I need to get a key", "I don't want to show weather info"]
  67. const weather = await this.generateAlert(message,options)
  68.  
  69. // Show a web view to claim the API key.
  70. if (weather == 1) {
  71. message = "On the next screen, sign up for OpenWeather. Find the API key, copy it, and close the web view. You will then be prompted to paste in the key."
  72. await this.generateAlert(message,["Continue"])
  73.  
  74. const webView = new WebView()
  75. webView.loadURL("https://openweathermap.org/home/sign_up")
  76. await webView.present()
  77. }
  78.  
  79. // We need the API key if we're showing weather.
  80. if (weather < 2 && !(await this.getWeatherKey(true))) { return }
  81.  
  82. if (!imported) { await this.setWidgetBackground() }
  83. this.writePreference("weather-cal-setup", "true")
  84.  
  85. 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."
  86. await this.generateAlert(message,["Show preview"])
  87. return this.previewValue()
  88. },
  89.  
  90. // Edit the widget settings.
  91. async editSettings(codeFilename, gitHubUrl) {
  92. const menu = {
  93. preview: "Show widget preview",
  94. background: "Change background",
  95. preferences: "Edit preferences",
  96. update: "Update code",
  97. share: "Export widget",
  98. other: "Other settings",
  99. exit: "Exit settings menu",
  100. }
  101. const menuOptions = [menu.preview, menu.background, menu.preferences, menu.update, menu.share, menu.other, menu.exit]
  102. const response = menuOptions[await this.generateAlert("Widget Setup",menuOptions)]
  103.  
  104. if (response == menu.preview) { return this.previewValue() }
  105. if (response == menu.background) { return await this.setWidgetBackground() }
  106. if (response == menu.preferences) { return await this.editPreferences() }
  107.  
  108. if (response == menu.update) {
  109. if (await this.generateAlert("Would you like to update the Weather Cal code? Your widgets will not be affected.",["Update", "Exit"])) return
  110. const success = await this.downloadCode(codeFilename, gitHubUrl)
  111. return await this.generateAlert(success ? "The update is now complete." : "The update failed. Please try again later.")
  112. }
  113.  
  114. if (response == menu.share) {
  115. const layout = this.fm.readString(this.fm.joinPath(this.fm.documentsDirectory(), this.name + ".js")).split('`')[1]
  116. const prefs = JSON.stringify(await this.getSettings())
  117. const bg = this.fm.readString(this.bgPath)
  118.  
  119. const widgetExport = `async function importWidget() {
  120. function makeAlert(message,options = ["OK"]) {
  121. const a = new Alert()
  122. a.message = message
  123. for (const option of options) { a.addAction(option) }
  124. return a
  125. }
  126. let fm = FileManager.local()
  127. fm = fm.isFileStoredIniCloud(module.filename) ? FileManager.iCloud() : fm
  128. const path = fm.joinPath(fm.documentsDirectory(), "Weather Cal code.js")
  129. const wc = fm.fileExists(path) ? fm.readString(path) : false
  130. const version = wc ? parseInt(wc.slice(wc.lastIndexOf("//") + 2).trim()) : false
  131. if (wc && (!version || version < 4)) { return await makeAlert("Please update Weather Cal before importing a widget.").present() }
  132. if ((await makeAlert("Do you want your widget to be named " + Script.name() + "?",["Yes, looks good","No, let me change it"]).present()) == 1) { return }
  133. fm.writeString(fm.joinPath(fm.libraryDirectory(), "weather-cal-preferences-" + Script.name()), '${prefs}')
  134. fm.writeString(fm.joinPath(fm.libraryDirectory(), "weather-cal-" + Script.name()), '${bg}')
  135. let code = await new Request('${this.widgetUrl}').loadString()
  136. let arr = code.split('\`')
  137. arr[1] = \`${layout}\`
  138. alert = makeAlert("Close this script and re-run it to finish setup.")
  139. fm.writeString(module.filename, arr.join('\`'))
  140. await alert.present()
  141. }
  142. await importWidget()
  143. Script.complete()`
  144.  
  145. const shouldUseQuickLook = await this.generateAlert("Your export is ready.",["Save to Files", "Display as text to copy"])
  146. if (shouldUseQuickLook) {
  147. QuickLook.present('/*\n\n\n\nTap the Share icon in the top right.\nThen tap "Copy" to copy all of this code.\nNow you can paste into a new script.\n\n\n\n*/\n' + widgetExport)
  148. } else {
  149. DocumentPicker.exportString(widgetExport, this.name + " export.js")
  150. }
  151. return
  152. }
  153.  
  154. if (response == menu.other) {
  155. const otherOptions = ["Re-enter API key", "Completely reset widget", "Exit"]
  156. const otherResponse = await this.generateAlert("Other settings",otherOptions)
  157.  
  158. // Set the API key.
  159. if (otherResponse == 0) { await this.getWeatherKey() }
  160.  
  161. // Reset the widget.
  162. else if (otherResponse == 1) {
  163. const alert = new Alert()
  164. alert.message = "Are you sure you want to completely reset this widget?"
  165. alert.addDestructiveAction("Reset")
  166. alert.addAction("Cancel")
  167.  
  168. if ((await alert.present()) == 0) {
  169. for (item of this.fm.listContents(this.fm.libraryDirectory())) {
  170. if (item.startsWith("weather-cal-") && item != "weather-cal-api-key" && item != "weather-cal-setup") {
  171. this.fm.remove(this.fm.joinPath(this.fm.libraryDirectory(), item))
  172. }
  173. }
  174. const success = await this.downloadCode(this.name, this.widgetUrl)
  175. const message = success ? "This script has been reset. Close the script and reopen it for the change to take effect." : "The reset failed."
  176. await this.generateAlert(message)
  177. }
  178. }
  179. }
  180. return
  181. },
  182.  
  183. // Get the weather key, optionally determining if it's the first run.
  184. async getWeatherKey(firstRun = false) {
  185. const returnVal = await this.promptForText("Paste your API key in the box below.",[""],["82c29fdbgd6aebbb595d402f8a65fabf"])
  186. const apiKey = returnVal.textFieldValue(0)
  187. 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"]) }
  188.  
  189. this.writePreference("weather-cal-api-key", apiKey)
  190. const req = new Request("https://api.openweathermap.org/data/2.5/onecall?lat=37.332280&lon=-122.010980&appid=" + apiKey)
  191. try { val = await req.loadJSON() } catch { val = { current: false } }
  192.  
  193. if (!val.current) {
  194. 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."
  195. await this.generateAlert(message,[firstRun ? "Continue" : "OK"])
  196.  
  197. } else if (val.current && !firstRun) {
  198. await this.generateAlert("The new key worked and was saved.")
  199. }
  200. return true
  201. },
  202.  
  203. // Set the background of the widget.
  204. async setWidgetBackground() {
  205. const options = ["Solid color", "Automatic gradient", "Custom gradient", "Image from Photos"]
  206. const backgroundType = await this.generateAlert("What type of background would you like for your widget?",options)
  207.  
  208. const background = this.fm.fileExists(this.bgPath) ? JSON.parse(this.fm.readString(this.bgPath)) : {}
  209. if (backgroundType == 0) {
  210. background.type = "color"
  211. const returnVal = await this.promptForText("Background Color",[background.color,background.dark],["Default color","Dark mode color (optional)"],"Enter the hex value of the background color you want. You can optionally choose a different background color for dark mode.")
  212. background.color = returnVal.textFieldValue(0)
  213. background.dark = returnVal.textFieldValue(1)
  214.  
  215. } else if (backgroundType == 1) {
  216. background.type = "auto"
  217.  
  218. } else if (backgroundType == 2) {
  219. background.type = "gradient"
  220. const returnVal = await this.promptForText("Gradient Colors",[background.initialColor,background.finalColor,background.initialDark,background.finalDark],["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.")
  221. background.initialColor = returnVal.textFieldValue(0)
  222. background.finalColor = returnVal.textFieldValue(1)
  223. background.initialDark = returnVal.textFieldValue(2)
  224. background.finalDark = returnVal.textFieldValue(3)
  225.  
  226. } else if (backgroundType == 3) {
  227. background.type = "image"
  228.  
  229. const directoryPath = this.fm.joinPath(this.fm.documentsDirectory(), "Weather Cal")
  230. if (!this.fm.fileExists(directoryPath) || !this.fm.isDirectory(directoryPath)) { this.fm.createDirectory(directoryPath) }
  231.  
  232. this.fm.writeImage(this.fm.joinPath(directoryPath, this.name + ".jpg"), await Photos.fromLibrary())
  233.  
  234. background.dark = !(await this.generateAlert("Would you like to use a different image in dark mode?",["Yes","No"]))
  235. if (background.dark) this.fm.writeImage(this.fm.joinPath(directoryPath, this.name + " (Dark).jpg"), await Photos.fromLibrary())
  236. }
  237.  
  238. this.writePreference(null, background, this.bgPath)
  239. return this.previewValue()
  240. },
  241.  
  242. // Load or reload a table full of preferences.
  243. async loadPrefsTable(table,category) {
  244. table.removeAllRows()
  245. for (settingName in category) {
  246. if (settingName == "name") continue
  247.  
  248. const row = new UITableRow()
  249. row.dismissOnSelect = false
  250. row.height = 55
  251.  
  252. const setting = category[settingName]
  253.  
  254. let valText
  255. if (Array.isArray(setting.val)) {
  256. valText = setting.val.map(a => a.title).join(", ")
  257.  
  258. } else if (setting.type == "fonts") {
  259. const item = setting.val
  260. const size = item.size.length ? `size ${item.size}` : ""
  261. const font = item.font.length ? ` ${item.font}` : ""
  262. const color = item.color.length ? ` (${item.color}${item.dark.length ? "/" + item.dark : ""})` : ""
  263. const caps = item.caps.length && item.caps != this.enum.caps.none ? ` - ${item.caps}` : ""
  264. valText = size + font + color + caps
  265.  
  266. } else if (typeof setting.val == "object") {
  267. for (subItem in setting.val) {
  268. const setupText = subItem + ": " + setting.val[subItem]
  269. valText = (valText ? valText + ", " : "") + setupText
  270. }
  271.  
  272. } else {
  273. valText = setting.val + ""
  274. }
  275.  
  276. const cell = row.addText(setting.name,valText)
  277. cell.subtitleColor = Color.gray()
  278.  
  279. // If there's no type, it's just text.
  280. if (!setting.type) {
  281. row.onSelect = async () => {
  282. const returnVal = await this.promptForText(setting.name,[setting.val],[],setting.description)
  283. setting.val = returnVal.textFieldValue(0).trim()
  284. await this.loadPrefsTable(table,category)
  285. }
  286.  
  287. } else if (setting.type == "enum") {
  288. row.onSelect = async () => {
  289. const returnVal = await this.generateAlert(setting.name,setting.options,setting.description)
  290. setting.val = setting.options[returnVal]
  291. await this.loadPrefsTable(table,category)
  292. }
  293.  
  294. } else if (setting.type == "bool") {
  295. row.onSelect = async () => {
  296. const returnVal = await this.generateAlert(setting.name,["true","false"],setting.description)
  297. setting.val = !returnVal
  298. await this.loadPrefsTable(table,category)
  299. }
  300.  
  301. } else if (setting.type == "fonts") {
  302. row.onSelect = async () => {
  303. const keys = ["size","color","dark","font"]
  304. const values = []
  305. for (key of keys) values.push(setting.val[key])
  306.  
  307. const options = ["Capitalization","Save and Close"]
  308. const prompt = await this.generatePrompt(setting.name,setting.description,options,values,keys)
  309. const returnVal = await prompt.present()
  310.  
  311. if (returnVal) {
  312. for (let i=0; i < keys.length; i++) {
  313. setting.val[keys[i]] = prompt.textFieldValue(i).trim()
  314. }
  315. } else {
  316. const capOptions = [this.enum.caps.upper,this.enum.caps.lower,this.enum.caps.title,this.enum.caps.none]
  317. setting.val["caps"] = capOptions[await this.generateAlert("Capitalization",capOptions)]
  318. }
  319.  
  320. await this.loadPrefsTable(table,category)
  321. }
  322.  
  323. } else if (setting.type == "multival") {
  324. row.onSelect = async () => {
  325.  
  326. // We need an ordered set.
  327. const map = new Map(Object.entries(setting.val))
  328. const keys = Array.from(map.keys())
  329. const returnVal = await this.promptForText(setting.name,Array.from(map.values()),keys,setting.description)
  330. for (let i=0; i < keys.length; i++) {
  331. setting.val[keys[i]] = returnVal.textFieldValue(i).trim()
  332. }
  333. await this.loadPrefsTable(table,category)
  334. }
  335.  
  336. } else if (setting.type == "multiselect") {
  337. row.onSelect = async () => {
  338.  
  339. // We need to pass sets to the function.
  340. const options = new Set(setting.options)
  341. const selected = new Set(setting.val.map ? setting.val.map(a => a.identifier) : [])
  342. const multiTable = new UITable()
  343.  
  344. await this.loadMultiTable(multiTable, options, selected)
  345. await multiTable.present()
  346.  
  347. setting.val = [...options].filter(option => [...selected].includes(option.identifier))
  348. await this.loadPrefsTable(table,category)
  349. }
  350. }
  351. table.addRow(row)
  352. }
  353. table.reload()
  354. },
  355.  
  356. // Load or reload a table with multi-select rows.
  357. async loadMultiTable(table,options,selected) {
  358. table.removeAllRows()
  359. for (const item of options) {
  360. const row = new UITableRow()
  361. row.dismissOnSelect = false
  362. row.height = 55
  363.  
  364. const isSelected = selected.has(item.identifier)
  365. row.backgroundColor = isSelected ? Color.dynamic(new Color("d8d8de"), new Color("2c2c2c")) : Color.dynamic(Color.white(), new Color("151517"))
  366.  
  367. if (item.color) {
  368. const colorCell = row.addText(isSelected ? "\u25CF" : "\u25CB")
  369. colorCell.titleColor = item.color
  370. colorCell.widthWeight = 1
  371. }
  372.  
  373. const titleCell = row.addText(item.title)
  374. titleCell.widthWeight = 15
  375.  
  376. row.onSelect = async () => {
  377. if (isSelected) { selected.delete(item.identifier) }
  378. else { selected.add(item.identifier) }
  379. await this.loadMultiTable(table,options,selected)
  380. }
  381. table.addRow(row)
  382. }
  383. table.reload()
  384. },
  385.  
  386. // Get the current settings for the widget or for editing.
  387. async getSettings(forEditing = false) {
  388. let settingsFromFile
  389. if (this.fm.fileExists(this.prefPath)) { settingsFromFile = JSON.parse(this.fm.readString(this.prefPath)) }
  390.  
  391. const settingsObject = await this.defaultSettings()
  392. for (category in settingsObject) {
  393. for (item in settingsObject[category]) {
  394.  
  395. // If the setting exists, use it. Otherwise, the default is used.
  396. let value = (settingsFromFile && settingsFromFile[category]) ? settingsFromFile[category][item] : undefined
  397. if (value == undefined) { value = settingsObject[category][item].val }
  398.  
  399. // Format the object correctly depending on where it will be used.
  400. if (forEditing) { settingsObject[category][item].val = value }
  401. else { settingsObject[category][item] = value }
  402. }
  403. }
  404. return settingsObject
  405. },
  406.  
  407. // Edit preferences of the widget.
  408. async editPreferences() {
  409. const settingsObject = await this.getSettings(true)
  410. const table = new UITable()
  411. table.showSeparators = true
  412.  
  413. for (categoryKey in settingsObject) {
  414. const row = new UITableRow()
  415. row.dismissOnSelect = false
  416.  
  417. const category = settingsObject[categoryKey]
  418. row.addText(category.name)
  419. row.onSelect = async () => {
  420. const subTable = new UITable()
  421. subTable.showSeparators = true
  422. await this.loadPrefsTable(subTable,category)
  423. await subTable.present()
  424. }
  425. table.addRow(row)
  426. }
  427. await table.present()
  428.  
  429. for (categoryKey in settingsObject) {
  430. for (item in settingsObject[categoryKey]) {
  431. if (item == "name") continue
  432. settingsObject[categoryKey][item] = settingsObject[categoryKey][item].val
  433. }
  434. }
  435. this.writePreference(null, settingsObject, this.prefPath)
  436. },
  437.  
  438. // Return the size of the widget preview.
  439. previewValue() {
  440. if (this.fm.fileExists(this.prefPath)) {
  441. const settingsObject = JSON.parse(this.fm.readString(this.prefPath))
  442. return settingsObject.widget.preview
  443. } else { return "large" }
  444. },
  445.  
  446. // Download a Scriptable script.
  447. async downloadCode(filename, url) {
  448. try {
  449. const codeString = await new Request(url).loadString()
  450. if (codeString.indexOf("// Variables used by Scriptable.") < 0) {
  451. return false
  452. } else {
  453. this.fm.writeString(this.fm.joinPath(this.fm.documentsDirectory(), filename + ".js"), codeString)
  454. return true
  455. }
  456. } catch {
  457. return false
  458. }
  459. },
  460.  
  461. // Generate an alert with the provided array of options.
  462. async generateAlert(title,options,message) {
  463. return await this.generatePrompt(title,message,options)
  464. },
  465.  
  466. // Default prompt for text field values.
  467. async promptForText(title,values,keys,message) {
  468. return await this.generatePrompt(title,message,null,values,keys)
  469. },
  470.  
  471. // Generic implementation of an alert.
  472. async generatePrompt(title,message,options,textvals,placeholders) {
  473. const alert = new Alert()
  474. alert.title = title
  475. if (message) alert.message = message
  476.  
  477. const buttons = options || ["OK"]
  478. for (button of buttons) { alert.addAction(button) }
  479.  
  480. if (!textvals) { return await alert.presentAlert() }
  481.  
  482. for (i=0; i < textvals.length; i++) {
  483. alert.addTextField(placeholders && placeholders[i] ? placeholders[i] : null,(textvals[i] || "") + "")
  484. }
  485.  
  486. if (!options) await alert.present()
  487. return alert
  488. },
  489.  
  490. // Write the value of a preference to disk.
  491. writePreference(name, value, inputPath = null) {
  492. const preference = typeof value == "string" ? value : JSON.stringify(value)
  493. this.fm.writeString(inputPath || this.fm.joinPath(this.fm.libraryDirectory(), name), preference)
  494. },
  495.  
  496. /*
  497. * Widget spacing, background, and construction
  498. * -------------------------------------------- */
  499.  
  500. // Create and return the widget.
  501. async createWidget(layout, name, iCloudInUse, custom) {
  502. if (!this.initialized) this.initialize(name, iCloudInUse)
  503.  
  504. // Determine if we're using the old or new setup.
  505. if (typeof layout == "object") {
  506. this.settings = layout
  507.  
  508. } else {
  509. this.settings = await this.getSettings()
  510. this.settings.layout = layout
  511. }
  512.  
  513. // Shared values.
  514. this.locale = this.settings.widget.locale
  515. this.padding = parseInt(this.settings.widget.padding)
  516. this.localization = this.settings.localization
  517. this.format = this.settings.font
  518. this.custom = custom
  519. this.darkMode = !(Color.dynamic(Color.white(),Color.black()).red)
  520.  
  521. if (!this.locale || this.locale == "" || this.locale == null) { this.locale = Device.locale() }
  522.  
  523. // Widget setup.
  524. this.widget = new ListWidget()
  525. this.widget.spacing = 0
  526.  
  527. const verticalPad = this.padding < 10 ? 10 - this.padding : 10
  528. const horizontalPad = this.padding < 15 ? 15 - this.padding : 15
  529.  
  530. const widgetPad = this.settings.widget.widgetPadding || {}
  531. const topPad = (widgetPad.top && widgetPad.top.length) ? parseInt(widgetPad.top) : verticalPad
  532. const leftPad = (widgetPad.left && widgetPad.left.length) ? parseInt(widgetPad.left) : horizontalPad
  533. const bottomPad = (widgetPad.bottom && widgetPad.bottom.length) ? parseInt(widgetPad.bottom) : verticalPad
  534. const rightPad = (widgetPad.right && widgetPad.right.length) ? parseInt(widgetPad.right) : horizontalPad
  535.  
  536. this.widget.setPadding(topPad, leftPad, bottomPad, rightPad)
  537.  
  538. // Background setup.
  539. const background = JSON.parse(this.fm.readString(this.bgPath))
  540.  
  541. if (custom && custom.background) {
  542. await custom.background(this.widget)
  543.  
  544. } else if (background.type == "color") {
  545. this.widget.backgroundColor = this.provideColor(background)
  546.  
  547. } else if (background.type == "auto") {
  548. const gradient = new LinearGradient()
  549. const gradientSettings = await this.setupGradient()
  550.  
  551. gradient.colors = gradientSettings.color()
  552. gradient.locations = gradientSettings.position()
  553. this.widget.backgroundGradient = gradient
  554.  
  555. } else if (background.type == "gradient") {
  556. const gradient = new LinearGradient()
  557. const initialColor = this.provideColor({ color: background.initialColor, dark: background.initialDark })
  558. const finalColor = this.provideColor({ color: background.finalColor, dark: background.finalDark })
  559.  
  560. gradient.colors = [initialColor, finalColor]
  561. gradient.locations = [0, 1]
  562. this.widget.backgroundGradient = gradient
  563.  
  564. } else if (background.type == "image") {
  565. const extension = (this.darkMode && background.dark && !this.settings.widget.instantDark ? " (Dark)" : "") + ".jpg"
  566. const imagePath = this.fm.joinPath(this.fm.joinPath(this.fm.documentsDirectory(), "Weather Cal"), name + extension)
  567.  
  568. if (this.fm.fileExists(imagePath)) {
  569. if (this.fm.isFileStoredIniCloud(imagePath)) { await this.fm.downloadFileFromiCloud(imagePath) }
  570. this.widget.backgroundImage = this.fm.readImage(imagePath)
  571.  
  572. } else if (config.runsInWidget) {
  573. this.widget.backgroundColor = Color.gray()
  574.  
  575. } else {
  576. this.generateAlert("Please choose a background image in the settings menu.")
  577. }
  578. }
  579.  
  580. // Construct the widget.
  581. this.currentRow = {}
  582. this.currentColumn = {}
  583. this.left()
  584.  
  585. this.usingASCII = undefined
  586. this.currentColumns = []
  587. this.rowNeedsSetup = false
  588.  
  589. for (rawLine of this.settings.layout.split(/\r?\n/)) {
  590. const line = rawLine.trim()
  591. if (line == '') { continue }
  592. if (this.usingASCII == undefined) {
  593. if (line.includes("row")) { this.usingASCII = false }
  594. if (line[0] == "-" && line[line.length-1] == "-") { this.usingASCII = true }
  595. }
  596. this.usingASCII ? await this.processASCIILine(line) : await this.executeItem(line)
  597. }
  598. return this.widget
  599. },
  600.  
  601. // Execute an item in the layout generator.
  602. async executeItem(item) {
  603. const itemArray = item.replace(/[.,]$/,"").split('(')
  604. const functionName = itemArray[0]
  605. const parameter = itemArray[1] ? itemArray[1].slice(0, -1) : null
  606.  
  607. if (this.custom && this.custom[functionName]) { return await this.custom[functionName](this.currentColumn, parameter) }
  608. if (this[functionName]) { return await this[functionName](this.currentColumn, parameter) }
  609. console.error("The " + functionName + " item in your layout is unavailable. Check for misspellings or other formatting issues. If you have any custom items, ensure they are set up correctly.")
  610. },
  611.  
  612. // Processes a single line of ASCII.
  613. async processASCIILine(line) {
  614.  
  615. // If it's a line, enumerate previous columns (if any) and set up the new row.
  616. if (line[0] == "-" && line[line.length-1] == "-") {
  617. if (this.currentColumns.length > 0) {
  618. for (col of this.currentColumns) {
  619. if (!col) { continue }
  620. this.column(this.currentColumn,col.width)
  621. for (item of col.items) { await this.executeItem(item) }
  622. }
  623. this.currentColumns = []
  624. }
  625. return this.rowNeedsSetup = true
  626. }
  627.  
  628. if (this.rowNeedsSetup) {
  629. this.row(this.currentColumn)
  630. this.rowNeedsSetup = false
  631. }
  632.  
  633. const items = line.split('|')
  634. for (var i=1; i < items.length-1; i++) {
  635.  
  636. if (!this.currentColumns[i]) { this.currentColumns[i] = { items: [] } }
  637. const column = this.currentColumns[i].items
  638.  
  639. const rawItem = items[i]
  640. const trimmedItem = rawItem.trim().split("(")[0]
  641.  
  642. // If it's not a widget item, it's a column width or a space.
  643. if (!(this[trimmedItem] || (this.custom && this.custom[trimmedItem]))) {
  644.  
  645. if (rawItem.match(/\s+\d+\s+/)) {
  646. const value = parseInt(trimmedItem)
  647. if (value) { this.currentColumns[i].width = value }
  648. continue
  649. }
  650.  
  651. const prevItem = column[column.length-1]
  652. if (trimmedItem == "" && (!prevItem || !prevItem.startsWith("space"))) {
  653. column.push("space")
  654. continue
  655. }
  656. }
  657.  
  658. const leading = rawItem.startsWith(" ")
  659. const trailing = rawItem.endsWith(" ")
  660. column.push((leading && trailing) ? "center" : (trailing ? "left" : "right"))
  661. column.push(rawItem.trim())
  662. }
  663. },
  664.  
  665. // Makes a new row on the widget.
  666. row(input, parameter) {
  667. this.currentRow = this.widget.addStack()
  668. this.currentRow.layoutHorizontally()
  669. this.currentRow.setPadding(0, 0, 0, 0)
  670. this.currentColumn.spacing = 0
  671. if (parameter) this.currentRow.size = new Size(0,parseInt(parameter))
  672. },
  673.  
  674. // Makes a new column on the widget.
  675. column(input, parameter) {
  676. this.currentColumn = this.currentRow.addStack()
  677. this.currentColumn.layoutVertically()
  678. this.currentColumn.setPadding(0, 0, 0, 0)
  679. this.currentColumn.spacing = 0
  680. if (parameter) this.currentColumn.size = new Size(parseInt(parameter),0)
  681. },
  682.  
  683. // Adds a space, with an optional amount.
  684. space(input, parameter) {
  685. if (parameter) input.addSpacer(parseInt(parameter))
  686. else input.addSpacer()
  687. },
  688.  
  689. // Create an aligned stack to add content to.
  690. align(column) {
  691. const alignmentStack = column.addStack()
  692. alignmentStack.layoutHorizontally()
  693.  
  694. const returnStack = this.currentAlignment(alignmentStack)
  695. returnStack.layoutVertically()
  696. return returnStack
  697. },
  698.  
  699. // Set the current alignment.
  700. setAlignment(left = false, right = false) {
  701. function alignment(alignmentStack) {
  702. if (right) alignmentStack.addSpacer()
  703. const returnStack = alignmentStack.addStack()
  704. if (left) alignmentStack.addSpacer()
  705. return returnStack
  706. }
  707. this.currentAlignment = alignment
  708. },
  709.  
  710. // Change the current alignment to right, left, or center.
  711. right() { this.setAlignment(false, true) },
  712. left() { this.setAlignment(true, false) },
  713. center() { this.setAlignment(true, true) },
  714.  
  715. /*
  716. * Data setup functions
  717. * -------------------------------------------- */
  718.  
  719. // Set up the event data object.
  720. async setupEvents() {
  721. const eventSettings = this.settings.events
  722. let calSetting = eventSettings.selectCalendars
  723. let calendars
  724.  
  725. // Old, manually-entered comma lists.
  726. if (typeof calSetting == "string") {
  727. calSetting = calSetting.trim()
  728. calendars = calSetting.length > 0 ? calSetting.split(",") : []
  729.  
  730. } else {
  731. calendars = calSetting
  732. }
  733.  
  734. let numberOfDays = parseInt(eventSettings.numberOfDays)
  735. numberOfDays = isNaN(numberOfDays) ? 1 : numberOfDays
  736.  
  737. // Complex due to support for old boolean values.
  738. let showFutureAt = parseInt(eventSettings.showTomorrow)
  739. showFutureAt = isNaN(showFutureAt) ? (eventSettings.showTomorrow ? 0 : 24) : showFutureAt
  740.  
  741. const events = await CalendarEvent.thisWeek([])
  742. const nextWeek = await CalendarEvent.nextWeek([])
  743. events.push(...nextWeek)
  744.  
  745. this.data.events = events.filter((event, index, array) => {
  746. if (!(index == array.findIndex(t => t.identifier == event.identifier && t.startDate.getTime() == event.startDate.getTime()))) { return false }
  747.  
  748. const diff = this.dateDiff(this.now, event.startDate)
  749. if (diff < 0 || diff > numberOfDays) { return false }
  750. if (diff > 0 && this.now.getHours() < showFutureAt) { return false }
  751.  
  752. if (calendars.length && !(calendars.some(a => a.identifier == event.calendar.identifier) || calendars.includes(event.calendar.title))) { return false }
  753. if (event.title.startsWith("Canceled:")) { return false }
  754. if (event.isAllDay) { return eventSettings.showAllDay }
  755.  
  756. const minutesAfter = parseInt(eventSettings.minutesAfter) * 60000 || 0
  757. return (event.startDate.getTime() + minutesAfter > this.now.getTime())
  758.  
  759. }).slice(0,parseInt(eventSettings.numberOfEvents))
  760. },
  761.  
  762. // Set up the reminders data object.
  763. async setupReminders() {
  764. const reminderSettings = this.settings.reminders
  765. let listSetting = reminderSettings.selectLists
  766. let lists
  767.  
  768. // Old, manually-entered comma lists.
  769. if (typeof listSetting == "string") {
  770. listSetting = listSetting.trim()
  771. lists = listSetting.length > 0 ? listSetting.split(",") : []
  772. } else {
  773. lists = listSetting
  774. }
  775.  
  776. const reminders = await Reminder.allIncomplete()
  777. reminders.sort(function(a, b) {
  778.  
  779. // Non-null due dates are prioritized.
  780. if (!a.dueDate && b.dueDate) return 1
  781. if (a.dueDate && !b.dueDate) return -1
  782. if (!a.dueDate && !b.dueDate) return 0
  783.  
  784. // Otherwise, earlier due dates go first.
  785. const aTime = a.dueDate.getTime()
  786. const bTime = b.dueDate.getTime()
  787.  
  788. if (aTime > bTime) return 1
  789. if (aTime < bTime) return -1
  790. return 0
  791. })
  792.  
  793. this.data.reminders = reminders.filter((reminder) => {
  794. if (lists.length && !(lists.some(a => a.identifier == reminder.calendar.identifier) || lists.includes(reminder.calendar.title))) { return false }
  795. if (!reminder.dueDate) { return reminderSettings.showWithoutDueDate }
  796. if (reminder.isOverdue) { return reminderSettings.showOverdue }
  797. if (reminderSettings.todayOnly) { return this.dateDiff(reminder.dueDate, this.now) == 0 }
  798. return true
  799. }).slice(0,parseInt(reminderSettings.numberOfReminders))
  800. },
  801.  
  802. // Set up the gradient for the widget background.
  803. async setupGradient() {
  804. if (!this.data.sun) { await this.setupSunrise() }
  805.  
  806. if (this.isNight(this.now)) {
  807. return {
  808. color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] },
  809. position() { return [-0.5, 0.2, 0.5, 1] },
  810. }
  811. }
  812. return {
  813. color() { return [new Color("3a8cc1"), new Color("90c0df")] },
  814. position() { return [0, 1] },
  815. }
  816. },
  817.  
  818. // Set up the location data object.
  819. async setupLocation() {
  820. const locationPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-location")
  821. const locationCache = this.getCache(locationPath, this.settings ? parseInt(this.settings.widget.updateLocation) : null)
  822. let location
  823.  
  824. if (!locationCache || locationCache.cacheExpired) {
  825. try { location = await Location.current() }
  826. catch { location = locationCache || { cacheExpired: true } }
  827.  
  828. try {
  829. const geocode = await Location.reverseGeocode(location.latitude, location.longitude, this.locale)
  830. location.locality = (geocode[0].locality || geocode[0].postalAddress.city) || geocode[0].administrativeArea
  831. } catch {
  832. location.locality = locationCache ? locationCache.locality : null
  833. }
  834.  
  835. // If (and only if) we have new data, write it to disk.
  836. if (!location.cacheExpired) this.fm.writeString(locationPath, JSON.stringify(location))
  837. }
  838. this.data.location = location || locationCache
  839. if (!this.data.location.latitude) return false
  840. return true
  841. },
  842.  
  843. // Set up the sun data object.
  844. async setupSunrise() {
  845. if (!this.data.location) { await this.setupLocation() }
  846. const location = this.data.location
  847. async function getSunData(date) { return await new Request("https://api.sunrise-sunset.org/json?lat=" + location.latitude + "&lng=" + location.longitude + "&formatted=0&date=" + date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()).loadJSON() }
  848.  
  849. const sunPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-sunrise")
  850. let sunData = this.getCache(sunPath, 60, 1440)
  851.  
  852. if (!sunData || sunData.cacheExpired || !sunData.results || sunData.results.length == 0) {
  853. try {
  854. sunData = await getSunData(this.now)
  855.  
  856. const tomorrowDate = new Date()
  857. tomorrowDate.setDate(this.now.getDate() + 1)
  858. const tomorrowData = await getSunData(tomorrowDate)
  859. sunData.results.tomorrow = tomorrowData.results.sunrise
  860.  
  861. this.fm.writeString(sunPath, JSON.stringify(sunData))
  862. } catch {}
  863. }
  864. this.data.sun = {}
  865. this.data.sun.sunrise = sunData ? new Date(sunData.results.sunrise).getTime() : null
  866. this.data.sun.sunset = sunData ? new Date(sunData.results.sunset).getTime() : null
  867. this.data.sun.tomorrow = sunData ? new Date(sunData.results.tomorrow).getTime() : null
  868. },
  869.  
  870. // Set up the weather data object.
  871. async setupWeather() {
  872. if (!this.data.location) { await this.setupLocation() }
  873.  
  874. const weatherPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-cache")
  875. let weatherData = this.getCache(weatherPath, 1, 60)
  876.  
  877. const forcedLocale = this.settings.weather.locale || ""
  878. let locale = forcedLocale.length ? forcedLocale : this.locale
  879.  
  880. const safeLocales = this.getOpenWeatherLocaleCodes()
  881. if (!forcedLocale.length && !safeLocales.includes(locale)) {
  882. const languages = [locale, ...locale.split("_"), ...locale.split("-"), Device.locale(), ...Device.locale().split("_"), ...Device.locale().split("-")]
  883. for (item of languages) {
  884. if (safeLocales.includes(item)) {
  885. locale = item
  886. break
  887. }
  888. }
  889. }
  890.  
  891. if (!weatherData || weatherData.cacheExpired) {
  892. try {
  893. const apiKey = this.fm.readString(this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-api-key")).replace(/\"/g,"")
  894. 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
  895. weatherData = await new Request(weatherReq).loadJSON()
  896. if (weatherData.cod) { weatherData = null }
  897. if (weatherData) { this.fm.writeString(weatherPath, JSON.stringify(weatherData)) }
  898. } catch {}
  899. }
  900.  
  901. // English continues using the "main" weather description.
  902. const english = (locale.split("_")[0] == "en")
  903.  
  904. this.data.weather = {}
  905. this.data.weather.currentTemp = weatherData ? weatherData.current.temp : null
  906. this.data.weather.currentCondition = weatherData ? weatherData.current.weather[0].id : 100
  907. this.data.weather.currentDescription = weatherData ? (english ? weatherData.current.weather[0].main : weatherData.current.weather[0].description) : "--"
  908. this.data.weather.todayHigh = weatherData ? weatherData.daily[0].temp.max : null
  909. this.data.weather.todayLow = weatherData ? weatherData.daily[0].temp.min : null
  910.  
  911. this.data.weather.forecast = []
  912. this.data.weather.hourly = []
  913. for (let i=0; i <= 7; i++) {
  914. 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 }
  915. this.data.weather.hourly[i] = weatherData ? ({Temp: weatherData.hourly[i].temp, Condition: weatherData.hourly[i].weather[0].id}) : { Temp: null, Condition: 100 }
  916. }
  917.  
  918. this.data.weather.tomorrowRain = weatherData ? weatherData.daily[1].pop * 100 : null
  919. this.data.weather.nextHourRain = weatherData ? weatherData.hourly[1].pop * 100 : null
  920. },
  921.  
  922. // Set up the COVID data object.
  923. async setupCovid() {
  924. const covidPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-covid")
  925. let covidData = this.getCache(covidPath, 15, 1440)
  926.  
  927. if (!covidData || covidData.cacheExpired) {
  928. try {
  929. covidData = await new Request("https://coronavirus-19-api.herokuapp.com/countries/" + this.settings.covid.country.trim()).loadJSON()
  930. this.fm.writeString(covidPath, JSON.stringify(covidData))
  931. } catch {}
  932. }
  933. this.data.covid = covidData || {}
  934. },
  935.  
  936. // Set up the news.
  937. async setupNews() {
  938. const newsPath = this.fm.joinPath(this.fm.libraryDirectory(), "weather-cal-news")
  939. let newsData = this.getCache(newsPath, 1, 1440)
  940.  
  941. if (!newsData || newsData.cacheExpired) {
  942. try {
  943. let rawData = await new Request(this.settings.news.url).loadString()
  944. rawData = getTag(rawData, "item", parseInt(this.settings.news.numberOfItems))
  945. if (!rawData || rawData.length == 0) { throw 0 }
  946.  
  947. newsData = []
  948. for (item of rawData) {
  949. const listing = {}
  950. listing.title = scrubString(getTag(item, "title")[0])
  951. listing.link = getTag(item, "link")[0]
  952. listing.date = new Date(getTag(item, "pubDate")[0])
  953. newsData.push(listing)
  954. }
  955. this.fm.writeString(newsPath, JSON.stringify(newsData))
  956. } catch {}
  957. }
  958. this.data.news = newsData || []
  959.  
  960. // Get one or many tags from a string.
  961. function getTag(string, tag, number = 1) {
  962. const open = "<" + tag + ">"
  963. const close = "</" + tag + ">"
  964. let returnVal = []
  965. let data = string
  966.  
  967. for (i = 0; i < number; i++) {
  968. const closeIndex = data.indexOf(close)
  969. if (closeIndex < 0) break
  970. returnVal.push(data.slice(data.indexOf(open)+open.length, closeIndex))
  971. data = data.slice(closeIndex + close.length)
  972. }
  973. return returnVal
  974. }
  975.  
  976. // Scrub a string so it's readable.
  977. function scrubString(val) {
  978. return val.replace(/^<!\[CDATA\[/,"").replace(/\]\]>$/,"").replace(/&#(.+?);/g,function(match,p1) {
  979. return String.fromCodePoint(p1)
  980. })
  981. }
  982. },
  983.  
  984. /*
  985. * Widget items
  986. * -------------------------------------------- */
  987.  
  988. // Display the date on the widget.
  989. async date(column) {
  990. const dateSettings = this.settings.date
  991. if (!this.data.events && dateSettings.dynamicDateSize) { await this.setupEvents() }
  992.  
  993. if (dateSettings.dynamicDateSize ? this.data.events.length : dateSettings.staticDateSize == "small") {
  994. this.provideText(this.formatDate(this.now,dateSettings.smallDateFormat), column, this.format.smallDate, true)
  995.  
  996. } else {
  997. const dateOneStack = this.align(column)
  998. const dateOne = this.provideText(this.formatDate(this.now,dateSettings.largeDateLineOne), dateOneStack, this.format.largeDate1)
  999. dateOneStack.setPadding(this.padding/2, this.padding, 0, this.padding)
  1000.  
  1001. const dateTwoStack = this.align(column)
  1002. const dateTwo = this.provideText(this.formatDate(this.now,dateSettings.largeDateLineTwo), dateTwoStack, this.format.largeDate2)
  1003. dateTwoStack.setPadding(0, this.padding, this.padding, this.padding)
  1004. }
  1005. },
  1006.  
  1007. // Display a time-based greeting on the widget.
  1008. greeting(column) {
  1009.  
  1010. // This function makes a greeting based on the time of day.
  1011. function makeGreeting(hour, localization) {
  1012. if (hour < 5) { return localization.nightGreeting }
  1013. if (hour < 12) { return localization.morningGreeting }
  1014. if (hour-12 < 5) { return localization.afternoonGreeting }
  1015. if (hour-12 < 10) { return localization.eveningGreeting }
  1016. return localization.nightGreeting
  1017. }
  1018. this.provideText(makeGreeting(this.now.getHours(), this.localization), column, this.format.greeting, true)
  1019. },
  1020.  
  1021. // Display events on the widget.
  1022. async events(column) {
  1023. if (!this.data.events) { await this.setupEvents() }
  1024. const eventSettings = this.settings.events
  1025.  
  1026. if (this.data.events.length == 0) {
  1027. if (eventSettings.noEventBehavior == "message" && this.localization.noEventMessage.length) { return this.provideText(this.localization.noEventMessage, column, this.format.noEvents, true) }
  1028. if (this[eventSettings.noEventBehavior]) { return await this[eventSettings.noEventBehavior](column) }
  1029. }
  1030.  
  1031. let currentStack
  1032. let currentDiff = 0
  1033. const numberOfEvents = this.data.events.length
  1034. const settingUrlExists = (eventSettings.url || "").length > 0
  1035. const showCalendarColor = eventSettings.showCalendarColor
  1036. const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle"
  1037.  
  1038. // Creates an event stack on the widget for a specific date diff.
  1039. function makeEventStack(diff, currentDate) {
  1040. const eventStack = column.addStack()
  1041. eventStack.layoutVertically()
  1042. eventStack.setPadding(0, 0, 0, 0)
  1043. const secondsForDay = Math.floor(currentDate.getTime() / 1000) - 978307200 + (diff * 86400)
  1044. eventStack.url = settingUrlExists ? eventSettings.url : "calshow:" + secondsForDay
  1045. currentStack = eventStack
  1046. }
  1047.  
  1048. makeEventStack(currentDiff,this.now)
  1049.  
  1050. for (let i = 0; i < numberOfEvents; i++) {
  1051. const event = this.data.events[i]
  1052. const diff = this.dateDiff(this.now, event.startDate)
  1053.  
  1054. if (diff != currentDiff) {
  1055. currentDiff = diff
  1056. makeEventStack(currentDiff,this.now)
  1057.  
  1058. const tomorrowText = this.localization.tomorrowLabel
  1059. const eventLabelText = (diff == 1 && tomorrowText.length) ? tomorrowText : this.formatDate(event.startDate,eventSettings.labelFormat)
  1060. this.provideText(eventLabelText.toUpperCase(), currentStack, this.format.eventLabel, true)
  1061. }
  1062.  
  1063. // Setting up the title row.
  1064. const titleStack = this.align(currentStack)
  1065. titleStack.layoutHorizontally()
  1066.  
  1067. if (showCalendarColor.length && showCalendarColor != "none" && !showCalendarColor.includes("right")) {
  1068. const colorItemText = this.provideTextSymbol(colorShape) + " "
  1069. const colorItem = this.provideText(colorItemText, titleStack, this.format.eventTitle)
  1070. colorItem.textColor = event.calendar.color
  1071. }
  1072.  
  1073. const showLocation = eventSettings.showLocation && event.location
  1074. const showTime = !event.isAllDay
  1075.  
  1076. const title = this.provideText(event.title.trim(), titleStack, this.format.eventTitle)
  1077. const titlePadding = (showLocation || showTime) ? this.padding/5 : this.padding
  1078. titleStack.setPadding(this.padding, this.padding, titlePadding, this.padding)
  1079. if (this.data.events.length >= 3) { title.lineLimit = 1 } // TODO: Make setting for this
  1080.  
  1081. if (showCalendarColor.length && showCalendarColor != "none" && showCalendarColor.includes("right")) {
  1082. const colorItemText = " " + this.provideTextSymbol(colorShape)
  1083. const colorItem = this.provideText(colorItemText, titleStack, this.format.eventTitle)
  1084. colorItem.textColor = event.calendar.color
  1085. }
  1086.  
  1087. // Setting up the location row.
  1088. if (showLocation) {
  1089. const locationStack = this.align(currentStack)
  1090. const location = this.provideText(event.location, locationStack, this.format.eventLocation)
  1091. location.lineLimit = 1
  1092. locationStack.setPadding(0, this.padding, showTime ? this.padding/5 : this.padding, this.padding)
  1093. }
  1094.  
  1095. if (event.isAllDay) { continue }
  1096.  
  1097. // Setting up the time row.
  1098. let timeText = this.formatTime(event.startDate)
  1099. if (eventSettings.showEventLength == "time") {
  1100. timeText += "–" + this.formatTime(event.endDate)
  1101.  
  1102. } else if (eventSettings.showEventLength == "duration") {
  1103. const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60)
  1104. const hours = Math.floor(duration/60)
  1105. const minutes = Math.floor(duration % 60)
  1106. const hourText = hours>0 ? hours + this.localization.durationHour : ""
  1107. const minuteText = minutes>0 ? minutes + this.localization.durationMinute : ""
  1108. timeText += " \u2022 " + hourText + (hourText.length && minuteText.length ? " " : "") + minuteText
  1109. }
  1110.  
  1111. const timeStack = this.align(currentStack)
  1112. const time = this.provideText(timeText, timeStack, this.format.eventTime)
  1113. timeStack.setPadding(0, this.padding, this.padding, this.padding)
  1114. }
  1115. },
  1116.  
  1117. // Display reminders on the widget.
  1118. async reminders(column) {
  1119. if (!this.data.reminders) { await this.setupReminders() }
  1120. const reminderSettings = this.settings.reminders
  1121.  
  1122. if (this.data.reminders.length == 0) {
  1123. if (reminderSettings.noRemindersBehavior == "message" && this.localization.noRemindersMessage.length) { return this.provideText(this.localization.noRemindersMessage, column, this.format.noReminders, true) }
  1124. if (this[reminderSettings.noRemindersBehavior]) { return await this[reminderSettings.noRemindersBehavior](column) }
  1125. }
  1126.  
  1127. const reminderStack = column.addStack()
  1128. reminderStack.layoutVertically()
  1129. reminderStack.setPadding(0, 0, 0, 0)
  1130. const settingUrl = reminderSettings.url || ""
  1131. reminderStack.url = (settingUrl.length > 0) ? settingUrl : "x-apple-reminderkit://REMCDReminder/"
  1132.  
  1133. const numberOfReminders = this.data.reminders.length
  1134. const showListColor = reminderSettings.showListColor
  1135. const colorShape = showListColor.includes("circle") ? "circle" : "rectangle"
  1136.  
  1137. for (let i = 0; i < numberOfReminders; i++) {
  1138. const reminder = this.data.reminders[i]
  1139.  
  1140. const titleStack = this.align(reminderStack)
  1141. titleStack.layoutHorizontally()
  1142.  
  1143. // TODO: Functionize for events and reminders
  1144. if (showListColor.length && showListColor != "none" && !showListColor.includes("right")) {
  1145. let colorItemText = this.provideTextSymbol(colorShape) + " "
  1146. let colorItem = this.provideText(colorItemText, titleStack, this.format.reminderTitle)
  1147. colorItem.textColor = reminder.calendar.color
  1148. }
  1149.  
  1150. const title = this.provideText(reminder.title.trim(), titleStack, this.format.reminderTitle)
  1151. titleStack.setPadding(this.padding, this.padding, this.padding/5, this.padding)
  1152.  
  1153. if (showListColor.length && showListColor != "none" && showListColor.includes("right")) {
  1154. let colorItemText = " " + this.provideTextSymbol(colorShape)
  1155. let colorItem = this.provideText(colorItemText, titleStack, this.format.reminderTitle)
  1156. colorItem.textColor = reminder.calendar.color
  1157. }
  1158.  
  1159. if (reminder.isOverdue) { title.textColor = Color.red() }
  1160. if (reminder.isOverdue || !reminder.dueDate) { continue }
  1161.  
  1162. let timeText
  1163. if (reminderSettings.useRelativeDueDate) {
  1164. const rdf = new RelativeDateTimeFormatter()
  1165. rdf.locale = this.locale
  1166. rdf.useNamedDateTimeStyle()
  1167. timeText = rdf.string(reminder.dueDate, this.now)
  1168.  
  1169. } else {
  1170. const df = new DateFormatter()
  1171. df.locale = this.locale
  1172.  
  1173. if (this.dateDiff(reminder.dueDate, this.now) == 0 && reminder.dueDateIncludesTime) { df.useNoDateStyle() }
  1174. else { df.useShortDateStyle() }
  1175.  
  1176. if (reminder.dueDateIncludesTime) { df.useShortTimeStyle() }
  1177. else { df.useNoTimeStyle() }
  1178.  
  1179. timeText = df.string(reminder.dueDate)
  1180. }
  1181.  
  1182. const timeStack = this.align(reminderStack)
  1183. const time = this.provideText(timeText, timeStack, this.format.eventTime)
  1184. timeStack.setPadding(0, this.padding, this.padding, this.padding)
  1185. }
  1186. },
  1187.  
  1188. // Display the current weather.
  1189. async current(column) {
  1190. if (!this.data.weather) { await this.setupWeather() }
  1191. if (!this.data.sun) { await this.setupSunrise() }
  1192.  
  1193. const [locationData, weatherData, sunData] = [this.data.location, this.data.weather, this.data.sun]
  1194. const weatherSettings = this.settings.weather
  1195.  
  1196. // Setting up the current weather stack.
  1197. const currentWeatherStack = column.addStack()
  1198. currentWeatherStack.layoutVertically()
  1199. currentWeatherStack.setPadding(0, 0, 0, 0)
  1200.  
  1201. const defaultUrl = "https://weather.com/" + this.locale + "/weather/today/l/" + locationData.latitude + "," + locationData.longitude
  1202. const settingUrl = weatherSettings.urlCurrent || ""
  1203. if (settingUrl.trim() != "none") { currentWeatherStack.url = (settingUrl.length > 0) ? settingUrl : defaultUrl }
  1204.  
  1205. // Displaying the main conditions.
  1206. if (weatherSettings.showLocation) { this.provideText(locationData.locality, currentWeatherStack, this.format.smallTemp, true) }
  1207.  
  1208. const mainConditionStack = this.align(currentWeatherStack)
  1209. const mainCondition = mainConditionStack.addImage(this.provideConditionSymbol(weatherData.currentCondition,this.isNight(this.now)))
  1210. mainCondition.imageSize = new Size(22,22) // TODO: Adjustable size
  1211. this.tintIcon(mainCondition, this.format.largeTemp)
  1212. mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : this.padding, this.padding, 0, this.padding)
  1213.  
  1214. const tempText = this.displayNumber(weatherData.currentTemp,"--") + "°"
  1215. if (weatherSettings.horizontalCondition) {
  1216. mainConditionStack.addSpacer(5)
  1217. mainConditionStack.layoutHorizontally()
  1218. mainConditionStack.centerAlignContent()
  1219. this.provideText(tempText, mainConditionStack, this.format.largeTemp)
  1220. }
  1221.  
  1222. if (weatherSettings.showCondition) {
  1223. const conditionTextStack = this.align(currentWeatherStack)
  1224. this.provideText(weatherData.currentDescription, conditionTextStack, this.format.smallTemp)
  1225. conditionTextStack.setPadding(this.padding, this.padding, 0, this.padding)
  1226. }
  1227.  
  1228. if (!weatherSettings.horizontalCondition) {
  1229. const tempStack = this.align(currentWeatherStack)
  1230. tempStack.setPadding(0, this.padding, 0, this.padding)
  1231. this.provideText(tempText, tempStack, this.format.largeTemp)
  1232. }
  1233.  
  1234. if (!weatherSettings.showHighLow) { return }
  1235.  
  1236. // Setting up the temp bar.
  1237. const tempBarStack = this.align(currentWeatherStack)
  1238. tempBarStack.layoutVertically()
  1239. tempBarStack.setPadding(0, this.padding, this.padding, this.padding)
  1240. tempBarStack.size = new Size(60,30)
  1241.  
  1242. const tempBar = tempBarStack.addImage(this.provideTempBar())
  1243. if (this.settings.widget.instantDark) this.tintIcon(tempBar, this.format.tinyTemp, true)
  1244. tempBar.size = new Size(50,0)
  1245.  
  1246. tempBarStack.addSpacer(1)
  1247.  
  1248. const highLowStack = tempBarStack.addStack()
  1249. highLowStack.layoutHorizontally()
  1250. this.provideText(this.displayNumber(weatherData.todayLow,"-"), highLowStack, this.format.tinyTemp)
  1251. highLowStack.addSpacer()
  1252. this.provideText(this.displayNumber(weatherData.todayHigh,"-"), highLowStack, this.format.tinyTemp)
  1253. },
  1254.  
  1255. // Display upcoming weather.
  1256. async future(column) {
  1257. if (!this.data.weather) { await this.setupWeather() }
  1258. if (!this.data.sun) { await this.setupSunrise() }
  1259.  
  1260. const [locationData, weatherData, sunData] = [this.data.location, this.data.weather, this.data.sun]
  1261. const weatherSettings = this.settings.weather
  1262.  
  1263. const futureWeatherStack = column.addStack()
  1264. futureWeatherStack.layoutVertically()
  1265. futureWeatherStack.setPadding(0, 0, 0, 0)
  1266.  
  1267. const showNextHour = (this.now.getHours() < parseInt(weatherSettings.tomorrowShownAtHour))
  1268.  
  1269. const defaultUrl = "https://weather.com/" + this.locale + "/weather/" + (showNextHour ? "today" : "tenday") +"/l/" + locationData.latitude + "," + locationData.longitude
  1270. const settingUrl = showNextHour ? (weatherSettings.urlFuture || "") : (weatherSettings.urlForecast || "")
  1271. if (settingUrl != "none") { futureWeatherStack.url = (settingUrl.length > 0) ? settingUrl : defaultUrl }
  1272.  
  1273. const subLabelStack = this.align(futureWeatherStack)
  1274. const subLabelText = showNextHour ? this.localization.nextHourLabel : this.localization.tomorrowLabel
  1275. const subLabel = this.provideText(subLabelText, subLabelStack, this.format.smallTemp)
  1276. subLabelStack.setPadding(0, this.padding, this.padding/2, this.padding)
  1277.  
  1278. const subConditionStack = this.align(futureWeatherStack)
  1279. subConditionStack.layoutHorizontally()
  1280. subConditionStack.centerAlignContent()
  1281. subConditionStack.setPadding(0, this.padding, this.padding, this.padding)
  1282.  
  1283. let nightCondition = false
  1284. if (showNextHour) { nightCondition = this.isNight(new Date(this.now.getTime() + (60*60*1000))) }
  1285.  
  1286. const subCondition = subConditionStack.addImage(this.provideConditionSymbol(showNextHour ? weatherData.hourly[1].Condition : weatherData.forecast[1].Condition,nightCondition))
  1287. const subConditionSize = showNextHour ? 14 : 18
  1288. subCondition.imageSize = new Size(subConditionSize, subConditionSize)
  1289. this.tintIcon(subCondition, this.format.smallTemp)
  1290. subConditionStack.addSpacer(5)
  1291.  
  1292. if (showNextHour) {
  1293. this.provideText(this.displayNumber(weatherData.hourly[1].Temp,"--") + "°", subConditionStack, this.format.smallTemp)
  1294.  
  1295. } else {
  1296. const tomorrowLine = subConditionStack.addImage(this.drawVerticalLine(this.provideColor(this.format.tinyTemp, 0.5), 20))
  1297. if (this.settings.widget.instantDark) this.tintIcon(tomorrowLine, this.format.tinyTemp, true)
  1298. tomorrowLine.imageSize = new Size(3,28)
  1299. subConditionStack.addSpacer(5)
  1300. const tomorrowStack = subConditionStack.addStack()
  1301. tomorrowStack.layoutVertically()
  1302.  
  1303. this.provideText(this.displayNumber(weatherData.forecast[1].High,"-"), tomorrowStack, this.format.tinyTemp)
  1304. tomorrowStack.addSpacer(4)
  1305. this.provideText(this.displayNumber(weatherData.forecast[1].Low,"-"), tomorrowStack, this.format.tinyTemp)
  1306. }
  1307.  
  1308. if (weatherSettings.showRain) {
  1309. const subRainStack = this.align(futureWeatherStack)
  1310. subRainStack.layoutHorizontally()
  1311. subRainStack.centerAlignContent()
  1312. subRainStack.setPadding(0, this.padding, this.padding, this.padding)
  1313.  
  1314. const subRain = subRainStack.addImage(SFSymbol.named("umbrella").image)
  1315. subRain.imageSize = new Size(subConditionSize, subConditionSize)
  1316. this.tintIcon(subRain, this.format.smallTemp, true)
  1317. subRainStack.addSpacer(5)
  1318.  
  1319. this.provideText(this.displayNumber(showNextHour ? weatherData.nextHourRain : weatherData.tomorrowRain,"--") + "%", subRainStack, this.format.smallTemp)
  1320. }
  1321. },
  1322.  
  1323. // Display forecast weather.
  1324. async forecast(column, hourly = false) {
  1325. if (!this.data.weather) { await this.setupWeather() }
  1326. if (!this.data.sun) { await this.setupSunrise() }
  1327. const [locationData, weatherData, sunData, weatherSettings] = [this.data.location, this.data.weather, this.data.sun, this.settings.weather]
  1328.  
  1329. // Set up the container stack and overall spacing.
  1330. const weatherStack = this.align(column)
  1331. const defaultUrl = "https://weather.com/" + this.locale + "/weather/" + (hourly ? "today" : "tenday") + "/l/" + locationData.latitude + "," + locationData.longitude
  1332. const settingUrl = hourly ? (weatherSettings.urlFuture || "") : (weatherSettings.urlForecast || "")
  1333. weatherStack.url = (settingUrl.length > 0) ? settingUrl : defaultUrl
  1334.  
  1335. const horizontal = hourly ? weatherSettings.horizontalHours : weatherSettings.horizontalForecast
  1336. const spacing = (weatherSettings.spacing ? parseInt(weatherSettings.spacing) : 0) + (horizontal ? 0 : 5)
  1337. const outsidePadding = this.padding > spacing ? this.padding - spacing : 0
  1338.  
  1339. if (horizontal) {
  1340. weatherStack.layoutHorizontally()
  1341. weatherStack.setPadding(this.padding, outsidePadding, this.padding, outsidePadding)
  1342. } else {
  1343. weatherStack.layoutVertically()
  1344. weatherStack.setPadding(outsidePadding, this.padding, outsidePadding, this.padding)
  1345. }
  1346.  
  1347. let startIndex = hourly ? 1 : (weatherSettings.showToday ? 1 : 2)
  1348. let endIndex = (hourly ? parseInt(weatherSettings.showHours) : parseInt(weatherSettings.showDays)) + startIndex
  1349. if (endIndex > 9) { endIndex = 9 }
  1350.  
  1351. const myDate = new Date()
  1352. if (!hourly && startIndex == 1) { myDate.setDate(myDate.getDate() - 1) }
  1353. const dateFormat = hourly ? weatherSettings.showHoursFormat : weatherSettings.showDaysFormat
  1354.  
  1355. // Loop through each individual unit.
  1356. const edgePadding = this.padding > spacing ? spacing : this.padding
  1357. const smallFontSize = (this.format.smallTemp && this.format.smallTemp.size) ? this.format.smallTemp.size : this.format.defaultText.size
  1358. const stackSize = hourly ? new Size(smallFontSize*3,0) : new Size(smallFontSize*2.64,0)
  1359.  
  1360. for (var i=startIndex; i < endIndex; i++) {
  1361. if (hourly) { myDate.setHours(myDate.getHours() + 1) }
  1362. else { myDate.setDate(myDate.getDate() + 1) }
  1363.  
  1364. const unitStack = weatherStack.addStack()
  1365. const dateStack = unitStack.addStack()
  1366. const initialSpace = (i == startIndex) ? edgePadding : spacing
  1367. const finalSpace = (i == endIndex-1) ? edgePadding : spacing
  1368.  
  1369. if (horizontal) {
  1370. unitStack.setPadding(0, initialSpace, 0, finalSpace)
  1371. unitStack.layoutVertically()
  1372.  
  1373. dateStack.addSpacer()
  1374. this.provideText(this.formatDate(myDate,dateFormat), dateStack, this.format.smallTemp)
  1375. dateStack.addSpacer()
  1376.  
  1377. } else {
  1378. unitStack.setPadding(initialSpace, 0, finalSpace, 0)
  1379. unitStack.layoutHorizontally()
  1380.  
  1381. dateStack.layoutHorizontally()
  1382. dateStack.setPadding(0, 0, 0, 0)
  1383. dateStack.size = stackSize
  1384.  
  1385. const dateText = this.provideText(this.formatDate(myDate,dateFormat), dateStack, this.format.smallTemp)
  1386. dateText.lineLimit = 1
  1387. dateText.minimumScaleFactor = 0.5
  1388. dateStack.addSpacer()
  1389. }
  1390.  
  1391. unitStack.centerAlignContent()
  1392. unitStack.addSpacer(5)
  1393.  
  1394. const conditionStack = unitStack.addStack()
  1395. conditionStack.centerAlignContent()
  1396. conditionStack.layoutHorizontally()
  1397. if (horizontal) { conditionStack.addSpacer() }
  1398.  
  1399. // Set up the container for the condition.
  1400. if (hourly) {
  1401. const subCondition = conditionStack.addImage(this.provideConditionSymbol(weatherData.hourly[i - 1].Condition, this.isNight(myDate)))
  1402. subCondition.imageSize = new Size(18,18)
  1403. this.tintIcon(subCondition, this.format.smallTemp)
  1404.  
  1405. if (horizontal) { conditionStack.addSpacer() }
  1406. unitStack.addSpacer(5)
  1407.  
  1408. const tempStack = unitStack.addStack()
  1409. tempStack.centerAlignContent()
  1410. tempStack.layoutHorizontally()
  1411.  
  1412. if (horizontal) { tempStack.addSpacer() }
  1413. const temp = this.provideText(this.displayNumber(weatherData.hourly[i - 1].Temp,"--") + "°", tempStack, this.format.smallTemp)
  1414. temp.lineLimit = 1
  1415. temp.minimumScaleFactor = 0.75
  1416. if (horizontal) {
  1417. temp.size = stackSize
  1418. tempStack.addSpacer()
  1419. }
  1420.  
  1421. } else {
  1422. const tinyFontSize = (this.format.tinyTemp && this.format.tinyTemp.size) ? this.format.tinyTemp.size : this.format.defaultText.size
  1423. conditionStack.size = new Size(0,tinyFontSize*2.64)
  1424.  
  1425. const conditionIcon = conditionStack.addImage(this.provideConditionSymbol(weatherData.forecast[i - 1].Condition, false))
  1426. conditionIcon.imageSize = new Size(18,18)
  1427. this.tintIcon(conditionIcon, this.format.smallTemp)
  1428. conditionStack.addSpacer(5)
  1429.  
  1430. const tempLine = conditionStack.addImage(this.drawVerticalLine(this.provideColor(this.format.tinyTemp, 0.5), 20))
  1431. if (this.settings.widget.instantDark) this.tintIcon(tempLine, this.format.tinyTemp, true)
  1432. tempLine.imageSize = new Size(3,28)
  1433. conditionStack.addSpacer(5)
  1434.  
  1435. let tempStack = conditionStack.addStack()
  1436. tempStack.layoutVertically()
  1437. tempStack.size = hourly ? new Size(smallFontSize*1,0) : new Size(smallFontSize*1,0)
  1438.  
  1439. const tempHigh = this.provideText(this.displayNumber(weatherData.forecast[i - 1].High,"-"), tempStack, this.format.tinyTemp)
  1440. tempHigh.lineLimit = 1
  1441. tempHigh.minimumScaleFactor = 0.6
  1442. tempStack.addSpacer(4)
  1443. const tempLow = this.provideText(this.displayNumber(weatherData.forecast[i - 1].Low,"-"), tempStack, this.format.tinyTemp)
  1444. tempLow.lineLimit = 1
  1445. tempLow.minimumScaleFactor = 0.6
  1446.  
  1447. if (horizontal) { conditionStack.addSpacer() }
  1448. }
  1449. }
  1450. },
  1451.  
  1452. // Allow both terms to be used.
  1453. async daily(column) { await this.forecast(column) },
  1454.  
  1455. // Display an hourly forecast.
  1456. async hourly(column) { await this.forecast(column, true) },
  1457.  
  1458. // Show the sunrise or sunset time.
  1459. async sunrise(column, forceSunset = false) {
  1460. if (!this.data.sun) { await this.setupSunrise() }
  1461. const [sunrise, sunset, tomorrow, current, sunSettings] = [this.data.sun.sunrise, this.data.sun.sunset, this.data.sun.tomorrow, this.now.getTime(), this.settings.sunrise]
  1462.  
  1463. const showWithin = parseInt(sunSettings.showWithin)
  1464. if (showWithin > 0 && !(Math.abs(this.now.getTime() - sunrise) / 60000 <= showWithin) && !(Math.abs(this.now.getTime() - sunset) / 60000 <= showWithin)) { return }
  1465.  
  1466. let timeToShow, symbolName
  1467. const showSunset = current > sunrise + 30*60*1000 && current < sunset + 30*60*1000
  1468.  
  1469. if (sunSettings.separateElements ? forceSunset : showSunset) {
  1470. symbolName = "sunset.fill"
  1471. timeToShow = sunset
  1472. } else {
  1473. symbolName = "sunrise.fill"
  1474. timeToShow = current > sunset ? tomorrow : sunrise
  1475. }
  1476.  
  1477. const sunriseStack = this.align(column)
  1478. sunriseStack.setPadding(this.padding/2, this.padding, this.padding/2, this.padding)
  1479. sunriseStack.layoutHorizontally()
  1480. sunriseStack.centerAlignContent()
  1481.  
  1482. sunriseStack.addSpacer(this.padding * 0.3)
  1483.  
  1484. const sunSymbol = sunriseStack.addImage(SFSymbol.named(symbolName).image)
  1485. sunSymbol.imageSize = new Size(22,22)
  1486. this.tintIcon(sunSymbol, this.format.sunrise) // TODO: Maybe function-ize this too?
  1487.  
  1488. sunriseStack.addSpacer(this.padding)
  1489.  
  1490. const time = this.provideText(timeToShow == null ? "--" : this.formatTime(new Date(timeToShow)), sunriseStack, this.format.sunrise)
  1491. },
  1492.  
  1493. // Allow for either term to be used.
  1494. async sunset(column) { return await this.sunrise(column, true) },
  1495.  
  1496. // Display COVID info on the widget.
  1497. async covid(column) {
  1498. if (!this.data.covid) { await this.setupCovid() }
  1499.  
  1500. const covidStack = this.align(column)
  1501. covidStack.setPadding(this.padding/2, this.padding, this.padding/2, this.padding)
  1502. covidStack.layoutHorizontally()
  1503. covidStack.centerAlignContent()
  1504. covidStack.url = this.settings.covid.url
  1505.  
  1506. covidStack.addSpacer(this.padding * 0.3)
  1507.  
  1508. const covidIcon = covidStack.addImage(SFSymbol.named("bandage").image)
  1509. covidIcon.imageSize = new Size(18,18)
  1510. this.tintIcon(covidIcon,this.format.covid,true)
  1511.  
  1512. covidStack.addSpacer(this.padding)
  1513.  
  1514. this.provideText(this.localization.covid.replace(/{(.*?)}/g, (match, $1) => {
  1515. let val = this.data.covid[$1]
  1516. if (val) val = new Intl.NumberFormat(this.locale.replace('_','-')).format(val)
  1517. return val || ""
  1518. }), covidStack, this.format.covid)
  1519. },
  1520.  
  1521. // Add custom text to the column.
  1522. text(column, input) {
  1523. if (!input || input == "") { return }
  1524. this.provideText(input, column, this.format.customText, true)
  1525. },
  1526.  
  1527. // Add a battery element to the widget.
  1528. battery(column) {
  1529. const batteryStack = this.align(column)
  1530. batteryStack.layoutHorizontally()
  1531. batteryStack.centerAlignContent()
  1532. batteryStack.setPadding(this.padding/2, this.padding, this.padding/2, this.padding)
  1533.  
  1534. const batteryIcon = batteryStack.addImage(this.provideBatteryIcon(Device.batteryLevel(),Device.isCharging()))
  1535. batteryIcon.imageSize = new Size(30,30)
  1536.  
  1537. const batteryLevel = Math.round(Device.batteryLevel() * 100)
  1538. if (batteryLevel > 20 || Device.isCharging() ) { this.tintIcon(batteryIcon,this.format.battery,true) }
  1539. else { batteryIcon.tintColor = Color.red() }
  1540.  
  1541. batteryStack.addSpacer(this.padding * 0.6)
  1542. this.provideText(batteryLevel + "%", batteryStack, this.format.battery)
  1543. },
  1544.  
  1545. // Display week number for current date.
  1546. week(column) {
  1547. const weekStack = this.align(column)
  1548. weekStack.setPadding(this.padding/2, this.padding, 0, this.padding)
  1549. weekStack.layoutHorizontally()
  1550. weekStack.centerAlignContent()
  1551.  
  1552. const currentThursday = new Date(this.now.getTime() +(3-((this.now.getDay()+6) % 7)) * 86400000)
  1553. const yearOfThursday = currentThursday.getFullYear()
  1554. const firstThursday = new Date(new Date(yearOfThursday,0,4).getTime() +(3-((new Date(yearOfThursday,0,4).getDay()+6) % 7)) * 86400000)
  1555. const weekNumber = Math.floor(1 + 0.5 + (currentThursday.getTime() - firstThursday.getTime()) / 86400000/7) + ""
  1556. this.provideText(this.localization.week + " " + weekNumber, weekStack, this.format.week)
  1557. },
  1558.  
  1559. // Display a symbol.
  1560. symbol(column, name) {
  1561. if (!name || !SFSymbol.named(name)) { return }
  1562.  
  1563. const symSettings = this.settings.symbol || {}
  1564. const symbolPad = symSettings.padding || {}
  1565. const topPad = (symbolPad.top && symbolPad.top.length) ? parseInt(symbolPad.top) : this.padding
  1566. const leftPad = (symbolPad.left && symbolPad.left.length) ? parseInt(symbolPad.left) : this.padding
  1567. const bottomPad = (symbolPad.bottom && symbolPad.bottom.length) ? parseInt(symbolPad.bottom) : this.padding
  1568. const rightPad = (symbolPad.right && symbolPad.right.length) ? parseInt(symbolPad.right) : this.padding
  1569.  
  1570. const symbolStack = this.align(column)
  1571. symbolStack.setPadding(topPad, leftPad, bottomPad, rightPad)
  1572.  
  1573. const symbol = symbolStack.addImage(SFSymbol.named(name).image)
  1574. const size = symSettings.size.length > 0 ? parseInt(symSettings.size) : column.size.width - (this.padding * 4)
  1575. symbol.imageSize = new Size(size, size)
  1576. if (symSettings.tintColor.length > 0) { symbol.tintColor = new Color(symSettings.tintColor) }
  1577. },
  1578.  
  1579. // Show news headlines.
  1580. async news(column) {
  1581. if (!this.data.news) { await this.setupNews() }
  1582. const newsSettings = this.settings.news
  1583.  
  1584. for (newsItem of this.data.news) {
  1585. const newsStack = column.addStack()
  1586. newsStack.setPadding(this.padding, this.padding, this.padding, this.padding)
  1587. newsStack.spacing = this.padding/5
  1588. newsStack.layoutVertically()
  1589. newsStack.url = newsItem.link
  1590.  
  1591. const titleStack = this.align(newsStack)
  1592. const title = this.provideText(newsItem.title, titleStack, this.format.newsTitle)
  1593. if (newsSettings.limitLineHeight) title.lineLimit = 1
  1594.  
  1595. if (!newsSettings.showDate) { continue }
  1596.  
  1597. const dateStack = this.align(newsStack)
  1598. let dateValue = new Date(newsItem.date)
  1599. let dateText
  1600. switch (newsSettings.showDate) {
  1601. case "relative":
  1602. const rdf = new RelativeDateTimeFormatter()
  1603. rdf.locale = this.locale
  1604. rdf.useNamedDateTimeStyle()
  1605. dateText = rdf.string(dateValue, this.now)
  1606. break
  1607. case "date":
  1608. dateText = this.formatDate(dateValue)
  1609. break
  1610. case "time":
  1611. dateText = dateText = this.formatTime(dateValue)
  1612. break
  1613. case "datetime":
  1614. dateText = this.formatDatetime(dateValue)
  1615. break
  1616. case "custom":
  1617. dateText = this.formatDate(dateValue, newsSettings.dateFormat)
  1618. }
  1619. if (dateText) this.provideText(dateText, dateStack, this.format.newsDate)
  1620. }
  1621. },
  1622.  
  1623. /*
  1624. * Helper functions
  1625. * -------------------------------------------- */
  1626.  
  1627. // Returns the supported OpenWeather locale codes.
  1628. getOpenWeatherLocaleCodes() {
  1629. return ["af","al","ar","az","bg","ca","cz","da","de","el","en","eu","fa","fi","fr","gl","he","hi","hr","hu","id","it","ja","kr","la","lt","mk","no","nl","pl","pt","pt_br","ro","ru","sv","se","sk","sl","sp","es","sr","th","tr","ua","uk","vi","zh_cn","zh_tw","zu"]
  1630. },
  1631.  
  1632. // Gets the cache.
  1633. getCache(path, minAge = -1, maxAge) {
  1634. if (!this.fm.fileExists(path)) return null
  1635. const cache = JSON.parse(this.fm.readString(path))
  1636. const age = (this.now.getTime() - this.fm.modificationDate(path).getTime())/60000
  1637.  
  1638. // Maximum ages must be explicitly defined.
  1639. if (Number.isInteger(maxAge) && age > maxAge) return null
  1640.  
  1641. // The cache is always expired if there's no acceptable minimum age.
  1642. if (minAge != -1 && (!minAge || age > minAge)) cache.cacheExpired = true
  1643. return cache
  1644. },
  1645.  
  1646. // Returns a rounded number string or the provided dummy text.
  1647. displayNumber(number,dummy = "-") { return (number == null ? dummy : Math.round(number).toString()) },
  1648.  
  1649. // Tints icons if needed or forced.
  1650. tintIcon(icon,format,force = false) {
  1651. const tintIcons = this.settings.widget.tintIcons
  1652. const never = tintIcons == this.enum.icons.never || !tintIcons
  1653. const notDark = tintIcons == this.enum.icons.dark && !this.darkMode && !this.settings.widget.instantDark
  1654. const notLight = tintIcons == this.enum.icons.light && this.darkMode && !this.settings.widget.instantDark
  1655. if (!force && (never || notDark || notLight)) { return }
  1656. icon.tintColor = this.provideColor(format)
  1657. },
  1658.  
  1659. // Determines if the provided date is at night.
  1660. isNight(dateInput) {
  1661. const timeValue = dateInput.getTime()
  1662. return (timeValue < this.data.sun.sunrise) || (timeValue > this.data.sun.sunset)
  1663. },
  1664.  
  1665. // Returns the difference in days between two dates. Adapted from StackOverflow.
  1666. dateDiff(first, second) {
  1667. const firstDate = new Date(first.getFullYear(), first.getMonth(), first.getDate(), 0, 0, 0)
  1668. const secondDate = new Date(second.getFullYear(), second.getMonth(), second.getDate(), 0, 0, 0)
  1669. return Math.round((secondDate-firstDate)/(1000*60*60*24))
  1670. },
  1671.  
  1672. // Convenience functions for dates and times.
  1673. formatTime(date) { return this.formatDate(date,null,false,true) },
  1674. formatDatetime(date) { return this.formatDate(date,null,true,true) },
  1675.  
  1676. // Format the date. If no format is provided, date-only is used by default.
  1677. formatDate(date,format,showDate = true, showTime = false) {
  1678. const df = new DateFormatter()
  1679. df.locale = this.locale
  1680. if (format) {
  1681. df.dateFormat = format
  1682. } else {
  1683. showDate ? df.useShortDateStyle() : df.useNoDateStyle()
  1684. showTime ? df.useShortTimeStyle() : df.useNoTimeStyle()
  1685. }
  1686. return df.string(date)
  1687. },
  1688.  
  1689. // Provide a text symbol with the specified shape.
  1690. provideTextSymbol(shape) {
  1691. if (shape.startsWith("rect")) { return "\u2759" }
  1692. if (shape == "circle") { return "\u2B24" }
  1693. return "\u2759"
  1694. },
  1695.  
  1696. // Provide a battery SFSymbol with accurate level drawn on top of it.
  1697. provideBatteryIcon(batteryLevel,charging = false) {
  1698. if (charging) { return SFSymbol.named("battery.100.bolt").image }
  1699.  
  1700. const batteryWidth = 87
  1701. const batteryHeight = 41
  1702.  
  1703. const draw = new DrawContext()
  1704. draw.opaque = false
  1705. draw.respectScreenScale = true
  1706. draw.size = new Size(batteryWidth, batteryHeight)
  1707.  
  1708. draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight))
  1709.  
  1710. const x = batteryWidth*0.1525
  1711. const y = batteryHeight*0.247
  1712. const width = batteryWidth*0.602
  1713. const height = batteryHeight*0.505
  1714.  
  1715. let level = batteryLevel
  1716. if (level < 0.05) { level = 0.05 }
  1717.  
  1718. const current = width * level
  1719. let radius = height/6.5
  1720.  
  1721. // When it gets low, adjust the radius to match.
  1722. if (current < (radius * 2)) { radius = current / 2 }
  1723.  
  1724. const barPath = new Path()
  1725. barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius)
  1726. draw.addPath(barPath)
  1727. draw.setFillColor(Color.black())
  1728. draw.fillPath()
  1729. return draw.getImage()
  1730. },
  1731.  
  1732. // Provide a symbol based on the condition.
  1733. provideConditionSymbol(cond,night) {
  1734. const symbols = {
  1735. "1": function() { return "exclamationmark.circle" },
  1736. "2": function() { return "cloud.bolt.rain.fill" },
  1737. "3": function() { return "cloud.drizzle.fill" },
  1738. "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" },
  1739. "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" },
  1740. "7": function() {
  1741. if (cond == 781) { return "tornado" }
  1742. if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
  1743. return night ? "cloud.fog.fill" : "sun.haze.fill"
  1744. },
  1745. "8": function() {
  1746. if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" }
  1747. if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
  1748. return "cloud.fill"
  1749. },
  1750. }
  1751. return SFSymbol.named(symbols[Math.floor(cond / 100)]()).image
  1752. },
  1753.  
  1754. // Provide a font based on the input.
  1755. provideFont(fontName, fontSize) {
  1756. const fontGenerator = {
  1757. ultralight() { return Font.ultraLightSystemFont(fontSize) },
  1758. light() { return Font.lightSystemFont(fontSize) },
  1759. regular() { return Font.regularSystemFont(fontSize) },
  1760. medium() { return Font.mediumSystemFont(fontSize) },
  1761. semibold() { return Font.semiboldSystemFont(fontSize) },
  1762. bold() { return Font.boldSystemFont(fontSize) },
  1763. heavy() { return Font.heavySystemFont(fontSize) },
  1764. black() { return Font.blackSystemFont(fontSize) },
  1765. italic() { return Font.italicSystemFont(fontSize) },
  1766. }
  1767. return fontGenerator[fontName] ? fontGenerator[fontName]() : new Font(fontName, fontSize)
  1768. },
  1769.  
  1770. // Add formatted text to a container.
  1771. provideText(string, stack, format, standardize = false) {
  1772. let container = stack
  1773. if (standardize) {
  1774. container = this.align(stack)
  1775. container.setPadding(this.padding, this.padding, this.padding, this.padding)
  1776. }
  1777.  
  1778. const capsEnum = this.enum.caps
  1779. function capitalize(text,caps) {
  1780. switch (caps) {
  1781. case (capsEnum.upper):
  1782. return text.toUpperCase()
  1783.  
  1784. case (capsEnum.lower):
  1785. return text.toLowerCase()
  1786.  
  1787. case (capsEnum.title):
  1788. return text.replace(/\w\S*/g,function(a) {
  1789. return a.charAt(0).toUpperCase() + a.substr(1).toLowerCase()
  1790. })
  1791. }
  1792. return text
  1793. }
  1794.  
  1795. const capFormat = (format && format.caps && format.caps.length) ? format.caps : this.format.defaultText.caps
  1796. const textItem = container.addText(capitalize(string,capFormat))
  1797.  
  1798. const textFont = (format && format.font && format.font.length) ? format.font : this.format.defaultText.font
  1799. const textSize = (format && format.size && parseInt(format.size)) ? format.size : this.format.defaultText.size
  1800. textItem.font = this.provideFont(textFont, parseInt(textSize))
  1801. textItem.textColor = this.provideColor(format)
  1802.  
  1803. return textItem
  1804. },
  1805.  
  1806. // Provide a color based on a format and the current dark mode state.
  1807. provideColor(format, alpha) {
  1808. const defaultText = this.format.defaultText
  1809. const lightColor = (format && format.color && format.color.length) ? format.color : defaultText.color
  1810. const defaultDark = (defaultText.dark && defaultText.dark.length) ? defaultText.dark : defaultText.color
  1811. const darkColor = (format && format.dark && format.dark.length) ? format.dark : defaultDark
  1812.  
  1813. if (this.settings.widget.instantDark) return Color.dynamic(new Color(lightColor, alpha), new Color(darkColor, alpha))
  1814. return new Color(this.darkMode && darkColor ? darkColor : lightColor, alpha)
  1815. },
  1816.  
  1817. // Draw the vertical line in the tomorrow view. - TODO: delete
  1818. drawVerticalLine(color, height) {
  1819.  
  1820. const width = 2
  1821.  
  1822. let draw = new DrawContext()
  1823. draw.opaque = false
  1824. draw.respectScreenScale = true
  1825. draw.size = new Size(width,height)
  1826.  
  1827. let barPath = new Path()
  1828. const barHeight = height
  1829. barPath.addRoundedRect(new Rect(0, 0, width, height), width/2, width/2)
  1830. draw.addPath(barPath)
  1831. draw.setFillColor(color)
  1832. draw.fillPath()
  1833.  
  1834. return draw.getImage()
  1835. },
  1836.  
  1837. // Provide the temp bar.
  1838. provideTempBar() {
  1839.  
  1840. const tempBarWidth = 200
  1841. const tempBarHeight = 20
  1842. const weatherData = this.data.weather
  1843.  
  1844. let percent = (weatherData.currentTemp - weatherData.todayLow) / (weatherData.todayHigh - weatherData.todayLow)
  1845. if (percent < 0) { percent = 0 }
  1846. else if (percent > 1) { percent = 1 }
  1847.  
  1848. const draw = new DrawContext()
  1849. draw.opaque = false
  1850. draw.respectScreenScale = true
  1851. draw.size = new Size(tempBarWidth, tempBarHeight)
  1852.  
  1853. const barPath = new Path()
  1854. const barHeight = tempBarHeight - 10
  1855. barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2)
  1856. draw.addPath(barPath)
  1857.  
  1858. draw.setFillColor(this.provideColor(this.format.tinyTemp, 0.5))
  1859. draw.fillPath()
  1860.  
  1861. const currPath = new Path()
  1862. currPath.addEllipse(new Rect((tempBarWidth - tempBarHeight) * percent, 0, tempBarHeight, tempBarHeight))
  1863. draw.addPath(currPath)
  1864. draw.setFillColor(this.provideColor(this.format.tinyTemp, 1))
  1865. draw.fillPath()
  1866.  
  1867. return draw.getImage()
  1868. },
  1869.  
  1870. // Return the default widget settings.
  1871. async defaultSettings() {
  1872. const settings = {
  1873. widget: {
  1874. name: "Overall settings",
  1875. locale: {
  1876. val: "",
  1877. name: "Locale code",
  1878. description: "Leave blank to match the device's locale.",
  1879. },
  1880. units: {
  1881. val: "imperial",
  1882. name: "Units",
  1883. description: "Use imperial for Fahrenheit or metric for Celsius.",
  1884. type: "enum",
  1885. options: ["imperial","metric"],
  1886. },
  1887. preview: {
  1888. val: "large",
  1889. name: "Widget preview size",
  1890. description: "Set the size of the widget preview displayed in the app.",
  1891. type: "enum",
  1892. options: ["small","medium","large"],
  1893. },
  1894. padding: {
  1895. val: "5",
  1896. name: "Item padding",
  1897. description: "The padding around each item. This also determines the approximate widget padding. Default is 5.",
  1898. },
  1899. widgetPadding: {
  1900. val: { top: "", left: "", bottom: "", right: "" },
  1901. name: "Custom widget padding",
  1902. type: "multival",
  1903. description: "The padding around the entire widget. By default, these values are blank and Weather Cal uses the item padding to determine these values. Transparent widgets often look best with these values at 0.",
  1904. },
  1905. tintIcons: {
  1906. val: this.enum.icons.never,
  1907. name: "Icons match text color",
  1908. description: "Decide when icons should match the color of the text around them.",
  1909. type: "enum",
  1910. options: [this.enum.icons.never,this.enum.icons.always,this.enum.icons.dark,this.enum.icons.light,],
  1911. },
  1912. updateLocation: {
  1913. val: "60",
  1914. name: "Location update frequency",
  1915. description: "How often, in minutes, to update the current location. Set to 0 to constantly update, or -1 to never update.",
  1916. },
  1917. instantDark: {
  1918. val: false,
  1919. name: "Instant dark mode (experimental)",
  1920. type: "bool",
  1921. description: "Instantly switch to dark mode. \u26A0\uFE0F This DOES NOT support dark mode image backgrounds or custom icon tint settings. \u26A0\uFE0F",
  1922. },
  1923. },
  1924. localization: {
  1925. name: "Localization and text customization",
  1926. morningGreeting: {
  1927. val: "Good morning.",
  1928. name: "Morning greeting",
  1929. },
  1930. afternoonGreeting: {
  1931. val: "Good afternoon.",
  1932. name: "Afternoon greeting",
  1933. },
  1934. eveningGreeting: {
  1935. val: "Good evening.",
  1936. name: "Evening greeting",
  1937. },
  1938. nightGreeting: {
  1939. val: "Good night.",
  1940. name: "Night greeting",
  1941. },
  1942. nextHourLabel: {
  1943. val: "Next hour",
  1944. name: "Label for next hour of weather",
  1945. },
  1946. tomorrowLabel: {
  1947. val: "Tomorrow",
  1948. name: "Label for tomorrow",
  1949. },
  1950. noEventMessage: {
  1951. val: "Enjoy the rest of your day.",
  1952. name: "No event message",
  1953. description: "The message shown when there are no more events for the day, if that setting is active.",
  1954. },
  1955. noRemindersMessage: {
  1956. val: "Tasks complete.",
  1957. name: "No reminders message",
  1958. description: "The message shown when there are no more reminders for the day, if that setting is active.",
  1959. },
  1960. durationMinute: {
  1961. val: "m",
  1962. name: "Duration label for minutes",
  1963. },
  1964. durationHour: {
  1965. val: "h",
  1966. name: "Duration label for hours",
  1967. },
  1968. covid: {
  1969. val: "{cases} cases, {deaths} deaths, {recovered} recoveries",
  1970. name: "COVID data text",
  1971. description: "Each {token} is replaced with the number from the data. The available tokens are: cases, todayCases, deaths, todayDeaths, recovered, active, critical, casesPerOneMillion, deathsPerOneMillion, totalTests, testsPerOneMillion"
  1972. },
  1973. week: {
  1974. val: "Week",
  1975. name: "Label for the week number",
  1976. },
  1977. },
  1978. font: {
  1979. name: "Text sizes, colors, and fonts",
  1980. defaultText: {
  1981. val: { size: "14", color: "ffffff", dark: "", font: "regular", caps: "" },
  1982. name: "Default font settings",
  1983. description: "These settings apply to all text on the widget that doesn't have a customized value.",
  1984. type: "fonts",
  1985. },
  1986. smallDate: {
  1987. val: { size: "17", color: "", dark: "", font: "semibold", caps: "" },
  1988. name: "Small date",
  1989. type: "fonts",
  1990. },
  1991. largeDate1: {
  1992. val: { size: "30", color: "", dark: "", font: "light", caps: "" },
  1993. name: "Large date, line 1",
  1994. type: "fonts",
  1995. },
  1996. largeDate2: {
  1997. val: { size: "30", color: "", dark: "", font: "light", caps: "" },
  1998. name: "Large date, line 2",
  1999. type: "fonts",
  2000. },
  2001. greeting: {
  2002. val: { size: "30", color: "", dark: "", font: "semibold", caps: "" },
  2003. name: "Greeting",
  2004. type: "fonts",
  2005. },
  2006. eventLabel: {
  2007. val: { size: "14", color: "", dark: "", font: "semibold", caps: "" },
  2008. name: "Event heading (used for the TOMORROW label)",
  2009. type: "fonts",
  2010. },
  2011. eventTitle: {
  2012. val: { size: "14", color: "", dark: "", font: "semibold", caps: "" },
  2013. name: "Event title",
  2014. type: "fonts",
  2015. },
  2016. eventLocation: {
  2017. val: { size: "14", color: "", dark: "", font: "", caps: "" },
  2018. name: "Event location",
  2019. type: "fonts",
  2020. },
  2021. eventTime: {
  2022. val: { size: "14", color: "ffffffcc", dark: "", font: "", caps: "" },
  2023. name: "Event time",
  2024. type: "fonts",
  2025. },
  2026. noEvents: {
  2027. val: { size: "30", color: "", dark: "", font: "semibold", caps: "" },
  2028. name: "No events message",
  2029. type: "fonts",
  2030. },
  2031. reminderTitle: {
  2032. val: { size: "14", color: "", dark: "", font: "", caps: "" },
  2033. name: "Reminder title",
  2034. type: "fonts",
  2035. },
  2036. reminderTime: {
  2037. val: { size: "14", color: "ffffffcc", dark: "", font: "", caps: "" },
  2038. name: "Reminder time",
  2039. type: "fonts",
  2040. },
  2041. noReminders: {
  2042. val: { size: "30", color: "", dark: "", font: "semibold", caps: "" },
  2043. name: "No reminders message",
  2044. type: "fonts",
  2045. },
  2046. newsTitle: {
  2047. val: { size: "14", color: "", dark: "", font: "", caps: "" },
  2048. name: "News item title",
  2049. type: "fonts",
  2050. },
  2051. newsDate: {
  2052. val: { size: "14", color: "ffffffcc", dark: "", font: "", caps: "" },
  2053. name: "News item date",
  2054. type: "fonts",
  2055. },
  2056. largeTemp: {
  2057. val: { size: "34", color: "", dark: "", font: "light", caps: "" },
  2058. name: "Large temperature label",
  2059. type: "fonts",
  2060. },
  2061. smallTemp: {
  2062. val: { size: "14", color: "", dark: "", font: "", caps: "" },
  2063. name: "Most text used in weather items",
  2064. type: "fonts",
  2065. },
  2066. tinyTemp: {
  2067. val: { size: "12", color: "", dark: "", font: "", caps: "" },
  2068. name: "Small text used in weather items",
  2069. type: "fonts",
  2070. },
  2071. customText: {
  2072. val: { size: "14", color: "", dark: "", font: "", caps: "" },
  2073. name: "User-defined text items",
  2074. type: "fonts",
  2075. },
  2076. battery: {
  2077. val: { size: "14", color: "", dark: "", font: "medium", caps: "" },
  2078. name: "Battery percentage",
  2079. type: "fonts",
  2080. },
  2081. sunrise: {
  2082. val: { size: "14", color: "", dark: "", font: "medium", caps: "" },
  2083. name: "Sunrise and sunset",
  2084. type: "fonts",
  2085. },
  2086. covid: {
  2087. val: { size: "14", color: "", dark: "", font: "medium", caps: "" },
  2088. name: "COVID data",
  2089. type: "fonts",
  2090. },
  2091. week: {
  2092. val: { size: "14", color: "", dark: "", font: "light", caps: "" },
  2093. name: "Week label",
  2094. type: "fonts",
  2095. },
  2096. },
  2097. date: {
  2098. name: "Date",
  2099. dynamicDateSize: {
  2100. val: true,
  2101. name: "Dynamic date size",
  2102. description: "If set to true, the date will become smaller when events are displayed.",
  2103. type: "bool",
  2104. },
  2105. staticDateSize: {
  2106. val: "small",
  2107. name: "Static date size",
  2108. description: "Set the date size shown when dynamic date size is not enabled.",
  2109. type: "enum",
  2110. options: ["small","large"],
  2111. },
  2112. smallDateFormat: {
  2113. val: "EEEE, MMMM d",
  2114. name: "Small date format",
  2115. },
  2116. largeDateLineOne: {
  2117. val: "EEEE,",
  2118. name: "Large date format, line 1",
  2119. },
  2120. largeDateLineTwo: {
  2121. val: "MMMM d",
  2122. name: "Large date format, line 2",
  2123. },
  2124. },
  2125. events: {
  2126. name: "Events",
  2127. numberOfEvents: {
  2128. val: "3",
  2129. name: "Maximum number of events shown",
  2130. },
  2131. minutesAfter: {
  2132. val: "5",
  2133. name: "Minutes after event begins",
  2134. description: "Number of minutes after an event begins that it should still be shown.",
  2135. },
  2136. showAllDay: {
  2137. val: false,
  2138. name: "Show all-day events",
  2139. type: "bool",
  2140. },
  2141. numberOfDays: {
  2142. val: "1",
  2143. name: "How many future days of events to show",
  2144. description: "How many days to show into the future. Set to 0 to show today's events only. The maximum is 7.",
  2145. },
  2146. labelFormat: {
  2147. val: "EEEE, MMMM d",
  2148. name: "Date format for future event days",
  2149. },
  2150. showTomorrow: {
  2151. val: "20",
  2152. name: "Future days shown at hour",
  2153. description: "The hour (in 24-hour time) to start showing events for tomorrow or beyond. Use 0 for always, 24 for never.",
  2154. },
  2155. showEventLength: {
  2156. val: "duration",
  2157. name: "Event length display style",
  2158. description: "Choose whether to show the duration, the end time, or no length information.",
  2159. type: "enum",
  2160. options: ["duration","time","none"],
  2161. },
  2162. showLocation: {
  2163. val: false,
  2164. name: "Show event location",
  2165. type: "bool",
  2166. },
  2167. selectCalendars: {
  2168. val: [],
  2169. name: "Calendars to show",
  2170. type: "multiselect",
  2171. options: await getFromCalendar(),
  2172. },
  2173. showCalendarColor: {
  2174. val: "rectangle left",
  2175. name: "Display calendar color",
  2176. description: "Choose the shape and location of the calendar color.",
  2177. type: "enum",
  2178. options: ["rectangle left","rectangle right","circle left","circle right","none"],
  2179. },
  2180. noEventBehavior: {
  2181. val: "message",
  2182. name: "Show when no events remain",
  2183. description: "When no events remain, show a hard-coded message, a time-based greeting, or nothing.",
  2184. type: "enum",
  2185. options: ["message","greeting","none"],
  2186. },
  2187. url: {
  2188. val: "",
  2189. name: "URL to open when tapped",
  2190. description: "Optionally provide a URL to open when this item is tapped. Leave blank to open the built-in Calendar app.",
  2191. },
  2192. },
  2193. reminders: {
  2194. name: "Reminders",
  2195. numberOfReminders: {
  2196. val: "3",
  2197. name: "Maximum number of reminders shown",
  2198. },
  2199. useRelativeDueDate: {
  2200. val: false,
  2201. name: "Use relative dates",
  2202. description: "Set to true for a relative due date (in 3 hours) instead of absolute (3:00 PM).",
  2203. type: "bool",
  2204. },
  2205. showWithoutDueDate: {
  2206. val: false,
  2207. name: "Show reminders without a due date",
  2208. type: "bool",
  2209. },
  2210. showOverdue: {
  2211. val: false,
  2212. name: "Show overdue reminders",
  2213. type: "bool",
  2214. },
  2215. todayOnly: {
  2216. val: false,
  2217. name: "Hide reminders due after today",
  2218. type: "bool",
  2219. },
  2220. selectLists: {
  2221. val: [],
  2222. name: "Lists to show",
  2223. type: "multiselect",
  2224. options: await getFromCalendar(true),
  2225. },
  2226. showListColor: {
  2227. val: "rectangle left",
  2228. name: "Display list color",
  2229. description: "Choose the shape and location of the list color.",
  2230. type: "enum",
  2231. options: ["rectangle left","rectangle right","circle left","circle right","none"],
  2232. },
  2233. noRemindersBehavior: {
  2234. val: "none",
  2235. name: "Show when no reminders remain",
  2236. description: "When no reminders remain, show a hard-coded message, a time-based greeting, or nothing.",
  2237. type: "enum",
  2238. options: ["message","greeting","none"],
  2239. },
  2240. url: {
  2241. val: "",
  2242. name: "URL to open when tapped",
  2243. description: "Optionally provide a URL to open when this item is tapped. Leave blank to open the built-in Reminders app.",
  2244. },
  2245. },
  2246. sunrise: {
  2247. name: "Sunrise and sunset",
  2248. showWithin: {
  2249. val: "",
  2250. name: "Limit times displayed",
  2251. description: "Set how many minutes before/after sunrise or sunset to show this element. Leave blank to always show.",
  2252. },
  2253. separateElements: {
  2254. val: false,
  2255. name: "Use separate sunrise and sunset elements",
  2256. description: "By default, the sunrise element changes between sunrise and sunset times automatically. Set to true for individual, hard-coded sunrise and sunset elements.",
  2257. type: "bool",
  2258. },
  2259. },
  2260. weather: {
  2261. name: "Weather",
  2262. locale: {
  2263. val: "",
  2264. name: "OpenWeather locale",
  2265. description: "If you are encountering issues with your weather data, try choosing an OpenWeather locale code.",
  2266. type: "enum",
  2267. options: this.getOpenWeatherLocaleCodes(),
  2268. },
  2269. showLocation: {
  2270. val: false,
  2271. name: "Show location name",
  2272. type: "bool",
  2273. },
  2274. horizontalCondition: {
  2275. val: false,
  2276. name: "Display the condition and temperature horizontally",
  2277. type: "bool",
  2278. },
  2279. showCondition: {
  2280. val: false,
  2281. name: "Show text value of the current condition",
  2282. type: "bool",
  2283. },
  2284. showHighLow: {
  2285. val: true,
  2286. name: "Show today's high and low temperatures",
  2287. type: "bool",
  2288. },
  2289. showRain: {
  2290. val: false,
  2291. name: "Show percent chance of rain",
  2292. type: "bool",
  2293. },
  2294. tomorrowShownAtHour: {
  2295. val: "20",
  2296. name: "When to switch to tomorrow's weather",
  2297. description: "Set the hour (in 24-hour time) to switch from the next hour to tomorrow's weather. Use 0 for always, 24 for never.",
  2298. },
  2299. spacing: {
  2300. val: "0",
  2301. name: "Spacing between daily or hourly forecast items",
  2302. },
  2303. horizontalHours: {
  2304. val: false,
  2305. name: "Display the hourly forecast horizontally",
  2306. type: "bool",
  2307. },
  2308. showHours: {
  2309. val: "3",
  2310. name: "Number of hours shown in the hourly forecast item",
  2311. },
  2312. showHoursFormat: {
  2313. val: "ha",
  2314. name: "Date format for the hourly forecast item",
  2315. },
  2316. horizontalForecast: {
  2317. val: false,
  2318. name: "Display the daily forecast horizontally",
  2319. type: "bool",
  2320. },
  2321. showDays: {
  2322. val: "3",
  2323. name: "Number of days shown in the daily forecast item",
  2324. },
  2325. showDaysFormat: {
  2326. val: "E",
  2327. name: "Date format for the daily forecast item",
  2328. },
  2329. showToday: {
  2330. val: false,
  2331. name: "Show today's weather in the daily forecast item",
  2332. type: "bool",
  2333. },
  2334. urlCurrent: {
  2335. val: "",
  2336. name: "URL to open when current weather is tapped",
  2337. description: "Optionally provide a URL to open when this item is tapped. Leave blank for the default.",
  2338. },
  2339. urlFuture: {
  2340. val: "",
  2341. name: "URL to open when hourly weather is tapped",
  2342. description: "Optionally provide a URL to open when this item is tapped. Leave blank for the default.",
  2343. },
  2344. urlForecast: {
  2345. val: "",
  2346. name: "URL to open when daily weather is tapped",
  2347. description: "Optionally provide a URL to open when this item is tapped. Leave blank for the default.",
  2348. },
  2349. },
  2350. covid: {
  2351. name: "COVID data",
  2352. country: {
  2353. val: "USA",
  2354. name: "Country for COVID information",
  2355. },
  2356. url: {
  2357. val: "https://covid19.who.int",
  2358. name: "URL to open when the COVID data is tapped",
  2359. },
  2360. },
  2361. symbol: {
  2362. name: "Symbols",
  2363. size: {
  2364. val: "18",
  2365. name: "Size",
  2366. description: "Size of each symbol. Leave blank to fill the width of the column.",
  2367. },
  2368. padding: {
  2369. val: { top: "", left: "", bottom: "", right: "" },
  2370. name: "Padding",
  2371. type: "multival",
  2372. description: "The padding around each symbol. Leave blank to use the default padding.",
  2373. },
  2374. tintColor: {
  2375. val: "ffffff",
  2376. name: "Tint color",
  2377. description: "The hex code color value to tint the symbols. Leave blank for the default tint.",
  2378. },
  2379. },
  2380. news: {
  2381. name: "News",
  2382. url: {
  2383. val: "http://rss.cnn.com/rss/cnn_topstories.rss",
  2384. name: "RSS feed link",
  2385. description: "The RSS feed link for the news to display."
  2386. },
  2387. numberOfItems: {
  2388. val: "1",
  2389. name: "Maximum number of news items shown",
  2390. },
  2391. limitLineHeight: {
  2392. val: false,
  2393. name: "Limit the height of each news item",
  2394. description: "Set this to true to limit each headline to a single line.",
  2395. type: "bool",
  2396. },
  2397. showDate: {
  2398. val: "none",
  2399. name: "Display the publish date for each news item",
  2400. description: "Use relative (5 minutes ago), date, time, date and time, a custom format, or none.",
  2401. type: "enum",
  2402. options: ["relative","date","time","datetime","custom","none"],
  2403. },
  2404. dateFormat: {
  2405. val: "H:mm",
  2406. name: "Date and/or time format for news items",
  2407. description: 'The format to use if the publish date setting is "formatted".',
  2408. },
  2409. },
  2410. }
  2411.  
  2412. async function getFromCalendar(forReminders) {
  2413. try { return await forReminders ? Calendar.forReminders() : Calendar.forEvents() }
  2414. catch { return [] }
  2415. }
  2416.  
  2417. return settings
  2418. },
  2419.  
  2420. enum: {
  2421. caps: {
  2422. upper: "ALL CAPS",
  2423. lower: "all lowercase",
  2424. title: "Title Case",
  2425. none: "None (Default)",
  2426. },
  2427. icons: {
  2428. never: "Never",
  2429. always: "Always",
  2430. dark: "In dark mode",
  2431. light: "In light mode",
  2432. }
  2433. },
  2434. }
  2435.  
  2436. module.exports = weatherCal
  2437.  
  2438. /*
  2439. * Detect the current module
  2440. * by Raymond Velasquez @supermamon
  2441. * -------------------------------------------- */
  2442.  
  2443. const moduleName = module.filename.match(/[^\/]+$/)[0].replace(".js","")
  2444. if (moduleName == Script.name()) {
  2445. await (async () => {
  2446. // Comment out the return to run a test.
  2447. return
  2448. const layout = `
  2449. row
  2450. column
  2451. `
  2452. const name = "Weather Cal Widget Builder"
  2453. await weatherCal.runSetup(name, true, "Weather Cal code", "https://raw.githubusercontent.com/mzeryck/Weather-Cal/main/weather-cal-code.js")
  2454. const w = await weatherCal.createWidget(layout, name, true)
  2455. w.presentLarge()
  2456. Script.complete()
  2457. })()
  2458. }
  2459.  
  2460. /*
  2461. * Don't modify the characters below this line.
  2462. * -------------------------------------------- */
  2463. //4
  2464.  
Advertisement
Add Comment
Please, Sign In to add comment