Advertisement
Guest User

ultimate daily

a guest
Nov 11th, 2020
72
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 51.10 KB | None | 0 0
  1. /*
  2. * SETUP
  3. * Use this section to set up the widget.
  4. * ======================================
  5. */
  6.  
  7. // To use weather, get a free API key at openweathermap.org/appid and paste it in between the quotation marks.
  8. const apiKey = "3e7a3342ab594faeb4bad4c6dc731c73"
  9.  
  10. // Set the locale code. Leave blank "" to match the device's locale. You can change the hard-coded text strings in the TEXT section below.
  11. let locale = "en"
  12.  
  13. // Set to true for fixed location, false to update location as you move around
  14. const lockLocation = true
  15.  
  16. // The size of the widget preview in the app.
  17. const widgetPreview = "large"
  18.  
  19. // Set to true for an image background, false for no image.
  20. const imageBackground = true
  21.  
  22. // Set to true to reset the widget's background image.
  23. const forceImageUpdate = false
  24.  
  25. // Set the padding around each item. Default is 5.
  26. const padding = 5
  27.  
  28. // Decide if icons should match the color of the text around them.
  29. const tintIcons = false
  30.  
  31. /*
  32. * LAYOUT
  33. * Decide what items to show on the widget.
  34. * ========================================
  35. */
  36.  
  37. // You always need to start with "row," and "column," items, but you can now add as many as you want.
  38. // Adding left, right, or center will align everything after that. The default alignment is left.
  39.  
  40. // You can add a flexible vertical space with "space," or a fixed-size space like this: "space(50)"
  41. // Align items to the top or bottom of columns by adding "space," before or after all items in the column.
  42.  
  43. // There are many possible items, including: date, greeting, events, current, future, battery, sunrise, and text("Your text here")
  44. // Make sure to always put a comma after each item.
  45.  
  46. const items = [
  47.  
  48. row,
  49.  
  50. column,
  51. greeting,
  52. date,
  53. // sunrise,
  54. space(),
  55.  
  56. column(90),
  57. current,
  58. battery,
  59. //sunrise,
  60.  
  61. row,
  62.  
  63. column,
  64. events,
  65.  
  66. column(90),
  67. //sunrise, use this one
  68. space(10),
  69. tasks,
  70.  
  71. ]
  72.  
  73.  
  74. /*
  75. * ITEM SETTINGS
  76. * Choose how each item is displayed.
  77. * ==================================
  78. */
  79.  
  80. // DATE
  81. // ====
  82. const dateSettings = {
  83.  
  84. // If set to true, date will become smaller when events are displayed.
  85. dynamicDateSize: false
  86.  
  87. // If the date is not dynamic, should it be large or small?
  88. ,staticDateSize: "large"
  89.  
  90. // Determine the date format for each date type. See docs.scriptable.app/dateformatter
  91. ,smallDateFormat: "EEEE, MMMM d"
  92. ,largeDateLineOne: "d"
  93. ,largeDateLineTwo: "EEEE"
  94. }
  95.  
  96. // EVENTS
  97. // ======
  98. const eventSettings = {
  99.  
  100. // How many events to show.
  101. numberOfEvents: 2
  102.  
  103. // Show all-day events.
  104. ,showAllDay: true
  105.  
  106. // Show tomorrow's events.
  107. ,showTomorrow: true
  108.  
  109. // Can be blank "" or set to "duration" or "time" to display how long an event is.
  110. ,showEventLength: ""
  111.  
  112. // Set which calendars for which to show events. Empty [] means all calendars.
  113. ,selectCalendars: []
  114.  
  115. // Leave blank "" for no color, or specify shape (circle, rectangle) and/or side (left, right).
  116. ,showCalendarColor: "rectangle left"
  117.  
  118. // When no events remain, show a hard-coded "message", a "greeting", or "none".
  119. ,noEventBehavior: "message"
  120. }
  121.  
  122. // SUNRISE
  123. // =======
  124. const sunriseSettings = {
  125.  
  126. // How many minutes before/after sunrise or sunset to show this element. 0 for always.
  127. showWithin: 0
  128. }
  129.  
  130. // WEATHER
  131. // =======
  132. const weatherSettings = {
  133.  
  134. // Set to imperial for Fahrenheit, or metric for Celsius
  135. units: "metric"
  136.  
  137. // Show the location of the current weather.
  138. ,showLocation: true
  139.  
  140. // Show the text description of the current conditions.
  141. ,showCondition: true
  142.  
  143. // Show today's high and low temperatures.
  144. ,showHighLow: false
  145.  
  146. // Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it.
  147. ,tomorrowShownAtHour: 19
  148. }
  149.  
  150. /*
  151. * TEXT
  152. * Change the language and formatting of text displayed.
  153. * =====================================================
  154. */
  155.  
  156. // You can change the language or wording of any text in the widget.
  157. const localizedText = {
  158.  
  159. // The text shown if you add a greeting item to the layout.
  160. nightGreeting: "good night"
  161. ,morningGreeting: "good morning"
  162. ,afternoonGreeting: "good afternoon"
  163. ,eveningGreeting: "good evening"
  164.  
  165. // The text shown if you add a future weather item to the layout, or tomorrow's events.
  166. ,nextHourLabel: "Next hour"
  167. ,tomorrowLabel: "Tomorrow"
  168.  
  169. // Shown when noEventBehavior is set to "message".
  170. ,noEventMessage: "Enjoy the rest of your day."
  171.  
  172. // The text shown after the hours and minutes of an event duration.
  173. ,durationMinute: "m"
  174. ,durationHour: "h"
  175.  
  176. }
  177.  
  178. // Set the font, size, and color of various text elements. Use iosfonts.com to find fonts to use. If you want to use the default iOS font, set the font name to one of the following: ultralight, light, regular, medium, semibold, bold, heavy, black, or italic.
  179. const textFormat = {
  180.  
  181. // Set the default font and color.
  182. defaultText: { size: 14, color: "ff00ff", font: "AvenirNext-Regular" },
  183.  
  184. // Any blank values will use the default.
  185. smallDate: { size: 17, color: "", font: "" },
  186. largeDate1: { size: 40, color: "ffffff", font: "AvenirNext-Medium" },
  187. largeDate2: { size: 24, color: "", font: "AvenirNext-DemiBold" },
  188.  
  189. greeting: { size: 20, color: "", font: "AvenirNext-Medium" },
  190. eventLabel: { size: 14, color: "", font: "" },
  191. eventTitle: { size: 14, color: "ffffff", font: "" },
  192. eventTime: { size: 14, color: "ffffffcc", font: "" },
  193. noEvents: { size: 30, color: "ffffff", font: "" },
  194.  
  195. largeTemp: { size: 28, color: "ffffff", font: "AvenirNext-Medium" },
  196. smallTemp: { size: 14, color: "", font: "" },
  197. tinyTemp: { size: 14, color: "ffffff", font: "" },
  198.  
  199. customText: { size: 14, color: "", font: "" },
  200.  
  201. battery: { size: 14, color: "ffffff", font: "" },
  202. sunrise: { size: 10, color: "", font: "" },
  203. }
  204.  
  205. /*
  206. * WIDGET CODE
  207. * Be more careful editing this section.
  208. * =====================================
  209. */
  210.  
  211. // Make sure we have a locale value.
  212. if (locale == "" || locale == null) { locale = Device.locale() }
  213.  
  214. // Declare the data variables.
  215. var eventData, locationData, sunData, weatherData
  216.  
  217. // Create global constants.
  218. const currentDate = new Date()
  219. const files = FileManager.local()
  220.  
  221. /*
  222. * CONSTRUCTION
  223. * ============
  224. */
  225.  
  226. // Set up the widget with padding.
  227. const widget = new ListWidget()
  228. const horizontalPad = padding < 10 ? 10 - padding : 10
  229. const verticalPad = padding < 15 ? 15 - padding : 15
  230. widget.setPadding(horizontalPad, verticalPad, horizontalPad, verticalPad)
  231. widget.spacing = 0
  232.  
  233. // Set up the global variables.
  234. var currentRow = {}
  235. var currentColumn = {}
  236.  
  237. // Set up the initial alignment.
  238. var currentAlignment = alignLeft
  239.  
  240. // Set up the global ASCII variables.
  241. var currentColumns = []
  242. var rowNeedsSetup = false
  243.  
  244. // It's ASCII time!
  245. if (typeof items[0] == 'string') {
  246. for (line of items[0].split(/\r?\n/)) { await processLine(line) }
  247. }
  248. // Otherwise, set up normally.
  249. else {
  250. for (item of items) { await item(currentColumn) }
  251. }
  252.  
  253. /*
  254. * BACKGROUND DISPLAY
  255. * ==================
  256. */
  257.  
  258. // If it's an image background, display it.
  259. if (imageBackground) {
  260.  
  261. // Determine if our image exists and when it was saved.
  262. const path = files.joinPath(files.documentsDirectory(), "weather-cal-image")
  263. const exists = files.fileExists(path)
  264.  
  265. // If it exists and an update isn't forced, use the cache.
  266. if (exists && (config.runsInWidget || !forceImageUpdate)) {
  267. widget.backgroundImage = files.readImage(path)
  268.  
  269. // If it's missing when running in the widget, use a gray background.
  270. } else if (!exists && config.runsInWidget) {
  271. widget.backgroundColor = Color.gray()
  272.  
  273. // But if we're running in app, prompt the user for the image.
  274. } else {
  275. const img = await Photos.fromLibrary()
  276. widget.backgroundImage = img
  277. files.writeImage(path, img)
  278. }
  279.  
  280. // If it's not an image background, show the gradient.
  281. } else {
  282. let gradient = new LinearGradient()
  283. let gradientSettings = await setupGradient()
  284.  
  285. gradient.colors = gradientSettings.color()
  286. gradient.locations = gradientSettings.position()
  287.  
  288. widget.backgroundGradient = gradient
  289. }
  290.  
  291. // Finish the widget and show a preview.
  292. Script.setWidget(widget)
  293. if (widgetPreview == "small") { widget.presentSmall() }
  294. else if (widgetPreview == "medium") { widget.presentMedium() }
  295. else if (widgetPreview == "large") { widget.presentLarge() }
  296. Script.complete()
  297.  
  298. /*
  299. * ASCII FUNCTIONS
  300. * Now isn't this a lot of fun?
  301. * ============================
  302. */
  303.  
  304. // Provide the named function.
  305. function provideFunction(name) {
  306. const functions = {
  307. space() { return space },
  308. left() { return left },
  309. right() { return right },
  310. center() { return center },
  311. date() { return date },
  312. greeting() { return greeting },
  313. events() { return events },
  314. current() { return current },
  315. future() { return future },
  316. battery() { return battery },
  317. sunrise() { return sunrise },
  318. }
  319. return functions[name]
  320. }
  321.  
  322. // Processes a single line of ASCII.
  323. async function processLine(lineInput) {
  324.  
  325. // Because iOS loves adding periods to everything.
  326. const line = lineInput.replace(/\.+/g,'')
  327.  
  328. // If it's blank, return.
  329. if (line.trim() == '') { return }
  330.  
  331. // If it's a line, enumerate previous columns (if any) and set up the new row.
  332. if (line[0] == '-' && line[line.length-1] == '-') {
  333. if (currentColumns.length > 0) { await enumerateColumns() }
  334. rowNeedsSetup = true
  335. return
  336. }
  337.  
  338. // If it's the first content row, finish the row setup.
  339. if (rowNeedsSetup) {
  340. row(currentColumn)
  341. rowNeedsSetup = false
  342. }
  343.  
  344. // If there's a number, this is a setup row.
  345. const setupRow = line.match(/\d+/)
  346.  
  347. // Otherwise, it has columns.
  348. const items = line.split('|')
  349.  
  350. // Iterate through each item.
  351. for (var i=1; i < items.length-1; i++) {
  352.  
  353. // If the current column doesn't exist, make it.
  354. if (!currentColumns[i]) { currentColumns[i] = { items: [] } }
  355.  
  356. // Now we have a column to add the items to.
  357. const column = currentColumns[i].items
  358.  
  359. // Get the current item and its trimmed version.
  360. const item = items[i]
  361. const trim = item.trim()
  362.  
  363. // If it's not a function, figure out spacing.
  364. if (!provideFunction(trim)) {
  365.  
  366. // If it's a setup row, whether or not we find the number, we keep going.
  367. if (setupRow) {
  368. const value = parseInt(trim, 10)
  369. if (value) { currentColumns[i].width = value }
  370. continue
  371. }
  372.  
  373. // If it's blank and we haven't already added a space, add one.
  374. const prevItem = column[column.length-1]
  375. if (trim == '' && (!prevItem || (prevItem && !prevItem.startsWith("space")))) {
  376. column.push("space")
  377. }
  378.  
  379. // Either way, we're done.
  380. continue
  381.  
  382. }
  383.  
  384. // Determine the alignment.
  385. const index = item.indexOf(trim)
  386. const length = item.slice(index,item.length).length
  387.  
  388. let align
  389. if (index > 0 && length > trim.length) { align = "center" }
  390. else if (index > 0) { align = "right" }
  391. else { align = "left" }
  392.  
  393. // Add the items to the column.
  394. column.push(align)
  395. column.push(trim)
  396. }
  397. }
  398.  
  399. // Runs the function names in each column.
  400. async function enumerateColumns() {
  401. if (currentColumns.length > 0) {
  402. for (col of currentColumns) {
  403.  
  404. // If it's null, go to the next one.
  405. if (!col) { continue }
  406.  
  407. // If there's a width, use the width function.
  408. if (col.width) {
  409. column(col.width)(currentColumn)
  410.  
  411. // Otherwise, create the column normally.
  412. } else {
  413. column(currentColumn)
  414. }
  415. for (item of col.items) {
  416. const func = provideFunction(item)()
  417. await func(currentColumn)
  418. }
  419. }
  420. currentColumns = []
  421. }
  422. }
  423.  
  424. /*
  425. * LAYOUT FUNCTIONS
  426. * These functions manage spacing and alignment.
  427. * =============================================
  428. */
  429.  
  430. // Makes a new row on the widget.
  431. function row(input = null) {
  432.  
  433. function makeRow() {
  434. currentRow = widget.addStack()
  435. currentRow.layoutHorizontally()
  436. currentRow.setPadding(0, 0, 0, 0)
  437. currentColumn.spacing = 0
  438.  
  439. // If input was given, make a column of that size.
  440. if (input > 0) { currentRow.size = new Size(0,input) }
  441. }
  442.  
  443. // If there's no input or it's a number, it's being called in the layout declaration.
  444. if (!input || typeof input == "number") { return makeRow }
  445.  
  446. // Otherwise, it's being called in the generator.
  447. else { makeRow() }
  448. }
  449.  
  450. // Makes a new column on the widget.
  451. function column(input = null) {
  452.  
  453. function makeColumn() {
  454. currentColumn = currentRow.addStack()
  455. currentColumn.layoutVertically()
  456. currentColumn.setPadding(0, 0, 0, 0)
  457. currentColumn.spacing = 0
  458.  
  459. // If input was given, make a column of that size.
  460. if (input > 0) { currentColumn.size = new Size(input,0) }
  461. }
  462.  
  463. // If there's no input or it's a number, it's being called in the layout declaration.
  464. if (!input || typeof input == "number") { return makeColumn }
  465.  
  466. // Otherwise, it's being called in the generator.
  467. else { makeColumn() }
  468. }
  469.  
  470. // Create an aligned stack to add content to.
  471. function align(column) {
  472.  
  473. // Add the containing stack to the column.
  474. let alignmentStack = column.addStack()
  475. alignmentStack.layoutHorizontally()
  476.  
  477. // Get the correct stack from the alignment function.
  478. let returnStack = currentAlignment(alignmentStack)
  479. returnStack.layoutVertically()
  480. return returnStack
  481. }
  482.  
  483. // Create a right-aligned stack.
  484. function alignRight(alignmentStack) {
  485. alignmentStack.addSpacer()
  486. let returnStack = alignmentStack.addStack()
  487. return returnStack
  488. }
  489.  
  490. // Create a left-aligned stack.
  491. function alignLeft(alignmentStack) {
  492. let returnStack = alignmentStack.addStack()
  493. alignmentStack.addSpacer()
  494. return returnStack
  495. }
  496.  
  497. // Create a center-aligned stack.
  498. function alignCenter(alignmentStack) {
  499. alignmentStack.addSpacer()
  500. let returnStack = alignmentStack.addStack()
  501. alignmentStack.addSpacer()
  502. return returnStack
  503. }
  504.  
  505. // This function adds a space, with an optional amount.
  506. function space(input = null) {
  507.  
  508. // This function adds a spacer with the input width.
  509. function spacer(column) {
  510.  
  511. // If the input is null or zero, add a flexible spacer.
  512. if (!input || input == 0) { column.addSpacer() }
  513.  
  514. // Otherwise, add a space with the specified length.
  515. else { column.addSpacer(input) }
  516. }
  517.  
  518. // If there's no input or it's a number, it's being called in the column declaration.
  519. if (!input || typeof input == "number") { return spacer }
  520.  
  521. // Otherwise, it's being called in the column generator.
  522. else { input.addSpacer() }
  523. }
  524.  
  525. // Change the current alignment to right.
  526. function right(x) { currentAlignment = alignRight }
  527.  
  528. // Change the current alignment to left.
  529. function left(x) { currentAlignment = alignLeft }
  530.  
  531. // Change the current alignment to center.
  532. function center(x) { currentAlignment = alignCenter }
  533.  
  534. /*
  535. * SETUP FUNCTIONS
  536. * These functions prepare data needed for items.
  537. * ==============================================
  538. */
  539.  
  540. // Set up Reminders.
  541.  
  542. async function tasks(column, alignment) {
  543.  
  544. function sortItems(first, second) {
  545. return first.dueDate - second.dueDate
  546. }
  547. // const yesterdayTasks = await Reminder.incompleteDueYesterday([])
  548. const todayTasks = await Reminder.incompleteDueToday([])
  549. const tomorrowTasks = await Reminder.incompleteDueTomorrow([])
  550.  
  551. /* if (yesterdayTasks.length > 0) {
  552.  
  553.  
  554. // const heading = widget.addText("Reminders:");
  555. yesterdayTasks.sort(sortItems).slice(0, 2).forEach(({ title, dueDate }) => {
  556.  
  557. if (dueDate < currentDate) {
  558.  
  559. const task = widget.addText(`❕❕ ${title}`);
  560. task.textColor = Color.white()
  561. task.font = Font.regularSystemFont(16)
  562. task.lineLimit = 1;
  563. const dueTime = String(dueDate)
  564. const options = { hour: 'numeric', minute: '2-digit'};
  565. const americanDateTime = new Intl.DateTimeFormat('en-US', options).format;
  566. const due = widget.addText(" " + americanDateTime(dueDate));
  567. due.textOpacity = .7
  568. due.textColor = Color.white()
  569. due.font = Font.mediumSystemFont(14)
  570.  
  571. widget.addSpacer(10);
  572.  
  573. }
  574. });
  575. } */
  576.  
  577. if (todayTasks.length + tomorrowTasks.length > 0) {
  578. var tasksStack = column.addStack()
  579. tasksStack.layoutVertically()
  580. tasksStack.url = 'x-apple-reminderkit://'
  581.  
  582. }
  583.  
  584.  
  585. if (todayTasks.length > 0) {
  586.  
  587. todayTasks.length = 2;
  588. // const heading = widget.addText("Reminders:");
  589. todayTasks.sort(sortItems).slice(0, 2).forEach(({ title, dueDate }) => {
  590.  
  591. if (dueDate < currentDate) {
  592.  
  593. const task = tasksStack.addText(`❕❕ ${title}`);
  594. task.textColor = Color.white()
  595. task.font = Font.regularSystemFont(16)
  596. task.lineLimit = 1;
  597. const dueTime = String(dueDate)
  598. const options = { hour: 'numeric', minute: '2-digit'};
  599. const americanDateTime = new Intl.DateTimeFormat('en-US', options).format;
  600. const due = tasksStack.addText(" " + americanDateTime(dueDate));
  601. due.textOpacity = .7
  602. due.textColor = Color.white()
  603. due.font = Font.mediumSystemFont(14)
  604.  
  605. tasksStack.addSpacer(10);
  606.  
  607. } else {
  608.  
  609. todayTasks.length = 2;
  610. const task = tasksStack.addText(`○ ${title}`);
  611. task.textColor = Color.white()
  612. task.font = Font.regularSystemFont(16)
  613. task.lineLimit = 1;
  614. const dueTime = String(dueDate)
  615. const options = { hour: 'numeric', minute: '2-digit'};
  616. const americanDateTime = new Intl.DateTimeFormat('en-US', options).format;
  617. const due = tasksStack.addText(" " + americanDateTime(dueDate));
  618. due.textOpacity = .7
  619. due.textColor = Color.white()
  620. due.font = Font.mediumSystemFont(14)
  621.  
  622. }
  623. });
  624. }
  625.  
  626. if (todayTasks.length == 0 && weatherSettings.tomorrowShownAtHour >= 19 && tomorrowTasks.length > 0) {
  627.  
  628. tomorrowTasks.length = 2;
  629. // const heading = widget.addText("Reminders:");
  630. tomorrowTasks.sort(sortItems).slice(0, 2).forEach(({ title, dueDate }) => {
  631.  
  632. if (dueDate > currentDate) {
  633.  
  634. const task = tasksStack.addText(`○ ${title}`);
  635. task.textColor = Color.white()
  636. task.font = Font.regularSystemFont(16)
  637. task.lineLimit = 1;
  638. const dueTime = String(dueDate)
  639. const options = { hour: 'numeric', minute: '2-digit'};
  640. const americanDateTime = new Intl.DateTimeFormat('en-US', options).format;
  641. const due = tasksStack.addText(" " + americanDateTime(dueDate));
  642. due.textOpacity = .7
  643. due.textColor = Color.white()
  644. due.font = Font.mediumSystemFont(14)
  645.  
  646. tasksStack.addSpacer(10);
  647.  
  648. }
  649. });
  650. }
  651.  
  652.  
  653. }
  654.  
  655. // Set up the eventData object.
  656. async function setupEvents() {
  657.  
  658. eventData = {}
  659. const calendars = eventSettings.selectCalendars
  660. const numberOfEvents = eventSettings.numberOfEvents
  661.  
  662. // Function to determine if an event should be shown.
  663. function shouldShowEvent(event) {
  664.  
  665. // If events are filtered and the calendar isn't in the selected calendars, return false.
  666. if (calendars.length && !calendars.includes(event.calendar.title)) { return false }
  667.  
  668. // Hack to remove canceled Office 365 events.
  669. if (event.title.startsWith("Canceled:")) { return false }
  670.  
  671. // If it's an all-day event, only show if the setting is active.
  672. if (event.isAllDay) { return eventSettings.showAllDay }
  673.  
  674. // Otherwise, return the event if it's in the future.
  675. return (event.startDate.getTime() > currentDate.getTime())
  676. }
  677.  
  678. // Determine which events to show, and how many.
  679. const todayEvents = await CalendarEvent.today([])
  680. let shownEvents = 0
  681. let futureEvents = []
  682.  
  683. for (const event of todayEvents) {
  684. if (shownEvents == numberOfEvents) { break }
  685. if (shouldShowEvent(event)) {
  686. futureEvents.push(event)
  687. shownEvents++
  688. }
  689. }
  690.  
  691. // If there's room and we need to, show tomorrow's events.
  692. let multipleTomorrowEvents = false
  693.  
  694. if (weatherSettings.tomorrowShownAtHour == 19) {
  695.  
  696. if (eventSettings.showTomorrow && shownEvents < numberOfEvents) {
  697.  
  698. const tomorrowEvents = await CalendarEvent.tomorrow([])
  699. for (const event of tomorrowEvents) {
  700. if (shownEvents == numberOfEvents) { break }
  701. if (shouldShowEvent(event)) {
  702.  
  703. // Add the tomorrow label prior to the first tomorrow event.
  704. if (!multipleTomorrowEvents) {
  705.  
  706. // The tomorrow label is pretending to be an event.
  707. futureEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isLabel: true })
  708. multipleTomorrowEvents = true
  709. }
  710.  
  711. // Show the tomorrow event and increment the counter.
  712. futureEvents.push(event)
  713. shownEvents++
  714. }
  715. }
  716. }
  717.  
  718. }
  719.  
  720. // Store the future events, and whether or not any events are displayed.
  721. eventData.futureEvents = futureEvents
  722. eventData.eventsAreVisible = (futureEvents.length > 0) && (eventSettings.numberOfEvents > 0)
  723. }
  724.  
  725. // Set up the gradient for the widget background.
  726. async function setupGradient() {
  727.  
  728. // Requirements: sunrise
  729. if (!sunData) { await setupSunrise() }
  730.  
  731. let gradient = {
  732. dawn: {
  733. color() { return [new Color("327ef6"), new Color("1c4475"), new Color("7f89fb")] },
  734. position() { return [0, 0.5, 1] },
  735. },
  736.  
  737. sunrise: {
  738. color() { return [new Color("ffcc2a"), new Color("ff9fa6"), new Color("f0b35e")] },
  739. position() { return [0, 0.8, 1.5] },
  740. },
  741.  
  742. midday: {
  743. color() { return [new Color("83cfff"), new Color("3fb3ff")] },
  744. position() { return [0, 1] },
  745. },
  746.  
  747. noon: {
  748. color() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] },
  749. position() { return [-0.2, 0.2, 1.5] },
  750. },
  751.  
  752. sunset: {
  753. color() { return [new Color("e7aa33"), new Color("fbd577"), new Color("ff980b")] },
  754. position() { return [0.1, 0.9, 1.2] },
  755. },
  756.  
  757. twilight: {
  758. color() { return [new Color("021033"), new Color("16296b"), new Color("151b69")] },
  759. position() { return [0, 0.5, 1] },
  760. },
  761.  
  762. night: {
  763. color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] },
  764. position() { return [-0.5, 0.2, 0.5, 1] },
  765. },
  766. }
  767.  
  768. const sunrise = sunData.sunrise
  769. const sunset = sunData.sunset
  770.  
  771. // Use sunrise or sunset if we're within 30min of it.
  772. if (closeTo(sunrise)<=15) { return gradient.sunrise }
  773. if (closeTo(sunset)<=15) { return gradient.sunset }
  774.  
  775. // In the 30min before/after, use dawn/twilight.
  776. if (closeTo(sunrise)<=45 && currentDate.getTime() < sunrise) { return gradient.dawn }
  777. if (closeTo(sunset)<=45 && currentDate.getTime() > sunset) { return gradient.twilight }
  778.  
  779. // Otherwise, if it's night, return night.
  780. if (isNight(currentDate)) { return gradient.night }
  781.  
  782. // If it's around noon, the sun is high in the sky.
  783. if (currentDate.getHours() == 12) { return gradient.noon }
  784.  
  785. // Otherwise, return the "typical" theme.
  786. return gradient.midday
  787. }
  788.  
  789. // Set up the locationData object.
  790. async function setupLocation() {
  791.  
  792. locationData = {}
  793. const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc")
  794.  
  795. // If our location is unlocked or cache doesn't exist, ask iOS for location.
  796. var readLocationFromFile = false
  797. if (!lockLocation || !files.fileExists(locationPath)) {
  798. try {
  799. const location = await Location.current()
  800. const geocode = await Location.reverseGeocode(location.latitude, location.longitude, locale)
  801. locationData.latitude = location.latitude
  802. locationData.longitude = location.longitude
  803. locationData.locality = geocode[0].locality
  804. files.writeString(locationPath, location.latitude + "|" + location.longitude + "|" + locationData.locality)
  805.  
  806. } catch(e) {
  807. // If we fail in unlocked mode, read it from the cache.
  808. if (!lockLocation) { readLocationFromFile = true }
  809.  
  810. // We can't recover if we fail on first run in locked mode.
  811. else { return }
  812. }
  813. }
  814.  
  815. // If our location is locked or we need to read from file, do it.
  816. if (lockLocation || readLocationFromFile) {
  817. const locationStr = files.readString(locationPath).split("|")
  818. locationData.latitude = locationStr[0]
  819. locationData.longitude = locationStr[1]
  820. locationData.locality = locationStr[2]
  821. }
  822. }
  823.  
  824. // Set up the sunData object.
  825. async function setupSunrise() {
  826.  
  827. // Requirements: location
  828. if (!locationData) { await setupLocation() }
  829.  
  830. async function getSunData(date) {
  831. const req = "https://api.sunrise-sunset.org/json?lat=" + locationData.latitude + "&lng=" + locationData.longitude + "&formatted=0&date=" + date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()
  832. const data = await new Request(req).loadJSON()
  833. return data
  834. }
  835.  
  836. // Set up the sunrise/sunset cache.
  837. const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sunrise")
  838. const sunCacheExists = files.fileExists(sunCachePath)
  839. const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0
  840. let sunDataRaw
  841.  
  842. // If cache exists and was created today, use cached data.
  843. if (sunCacheExists && sameDay(currentDate, sunCacheDate)) {
  844. const sunCache = files.readString(sunCachePath)
  845. sunDataRaw = JSON.parse(sunCache)
  846. }
  847.  
  848. // Otherwise, get the data from the server.
  849. else {
  850.  
  851. sunDataRaw = await getSunData(currentDate)
  852.  
  853. // Calculate tomorrow's date and get tomorrow's data.
  854. let tomorrowDate = new Date()
  855. tomorrowDate.setDate(currentDate.getDate() + 1)
  856. const tomorrowData = await getSunData(tomorrowDate)
  857. sunDataRaw.results.tomorrow = tomorrowData.results.sunrise
  858.  
  859. // Cache the file.
  860. files.writeString(sunCachePath, JSON.stringify(sunDataRaw))
  861. }
  862.  
  863. // Store the timing values.
  864. sunData = {}
  865. sunData.sunrise = new Date(sunDataRaw.results.sunrise).getTime()
  866. sunData.sunset = new Date(sunDataRaw.results.sunset).getTime()
  867. sunData.tomorrow = new Date(sunDataRaw.results.tomorrow).getTime()
  868. }
  869.  
  870. // Set up the weatherData object.
  871. async function setupWeather() {
  872.  
  873. // Requirements: location
  874. if (!locationData) { await setupLocation() }
  875.  
  876. // Set up the cache.
  877. const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache")
  878. const cacheExists = files.fileExists(cachePath)
  879. const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
  880. var weatherDataRaw
  881.  
  882. // If cache exists and it's been less than 60 seconds since last request, use cached data.
  883. if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) {
  884. const cache = files.readString(cachePath)
  885. weatherDataRaw = JSON.parse(cache)
  886.  
  887. // Otherwise, use the API to get new weather data.
  888. } else {
  889. const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + locationData.latitude + "&lon=" + locationData.longitude + "&exclude=minutely,alerts&units=" + weatherSettings.units + "&lang=" + locale + "&appid=" + apiKey
  890. weatherDataRaw = await new Request(weatherReq).loadJSON()
  891. files.writeString(cachePath, JSON.stringify(weatherDataRaw))
  892. }
  893.  
  894. // Store the weather values.
  895. weatherData = {}
  896. weatherData.currentTemp = weatherDataRaw.current.temp
  897. weatherData.currentCondition = weatherDataRaw.current.weather[0].id
  898. weatherData.currentDescription = weatherDataRaw.current.weather[0].main
  899. weatherData.todayHigh = weatherDataRaw.daily[0].temp.max
  900. weatherData.todayLow = weatherDataRaw.daily[0].temp.min
  901.  
  902. weatherData.nextHourTemp = weatherDataRaw.hourly[1].temp
  903. weatherData.nextHourCondition = weatherDataRaw.hourly[1].weather[0].id
  904.  
  905. weatherData.tomorrowHigh = weatherDataRaw.daily[1].temp.max
  906. weatherData.tomorrowLow = weatherDataRaw.daily[1].temp.min
  907. weatherData.tomorrowCondition = weatherDataRaw.daily[1].weather[0].id
  908. }
  909.  
  910. /*
  911. * WIDGET ITEMS
  912. * These functions display items on the widget.
  913. * ============================================
  914. */
  915.  
  916. // Display the date on the widget.
  917. async function date(column) {
  918.  
  919. // Requirements: events (if dynamicDateSize is enabled)
  920. if (!eventData && dateSettings.dynamicDateSize) { await setupEvents() }
  921.  
  922. // Set up the date formatter and set its locale.
  923. let df = new DateFormatter()
  924. df.locale = locale
  925.  
  926. // Show small if it's hard coded, or if it's dynamic and events are visible.
  927. if (dateSettings.staticDateSize == "small" || (dateSettings.dynamicDateSize && eventData.eventsAreVisible)) {
  928. let dateStack = align(column)
  929. dateStack.setPadding(padding, padding, padding, padding)
  930.  
  931. df.dateFormat = dateSettings.smallDateFormat
  932. let dateText = provideText(df.string(currentDate), dateStack, textFormat.smallDate)
  933.  
  934. // Otherwise, show the large date.
  935. } else {
  936. let dateOneStack = align(column)
  937. df.dateFormat = dateSettings.largeDateLineOne
  938. let dateOne = provideText(df.string(currentDate), dateOneStack, textFormat.largeDate1)
  939. dateOneStack.setPadding(padding/2, padding, 0, padding)
  940.  
  941. let dateTwoStack = align(column)
  942. df.dateFormat = dateSettings.largeDateLineTwo
  943. let dateTwo = provideText(df.string(currentDate), dateTwoStack, textFormat.largeDate2)
  944. dateTwoStack.setPadding(0, padding, padding, padding)
  945. }
  946. }
  947.  
  948. // Display a time-based greeting on the widget.
  949. async function greeting(column) {
  950.  
  951. // This function makes a greeting based on the time of day.
  952. function makeGreeting() {
  953. const hour = currentDate.getHours()
  954. if (hour < 5) { return localizedText.nightGreeting }
  955. if (hour < 12) { return localizedText.morningGreeting }
  956. if (hour-12 < 5) { return localizedText.afternoonGreeting }
  957. if (hour-12 < 10) { return localizedText.eveningGreeting }
  958. return localizedText.nightGreeting
  959. }
  960.  
  961. // Set up the greeting.
  962. let greetingStack = align(column)
  963. let greeting = provideText(makeGreeting(), greetingStack, textFormat.greeting)
  964. greetingStack.setPadding(padding, padding, padding, padding)
  965. }
  966.  
  967. // Display events on the widget.
  968. async function events(column) {
  969.  
  970. // Requirements: events
  971. if (!eventData) { await setupEvents() }
  972.  
  973. // If no events are visible, figure out what to do.
  974. if (!eventData.eventsAreVisible) {
  975. const display = eventSettings.noEventBehavior
  976.  
  977. // If it's a greeting, let the greeting function handle it.
  978. if (display == "greeting") { return await greeting(column) }
  979.  
  980. // If it's a message, get the localized text.
  981. if (display == "message" && localizedText.noEventMessage.length) {
  982. const messageStack = align(column)
  983. messageStack.setPadding(padding, padding, padding, padding)
  984. provideText(localizedText.noEventMessage, messageStack, textFormat.noEvents)
  985. }
  986.  
  987. // Whether or not we displayed something, return here.
  988. return
  989. }
  990.  
  991. // Set up the event stack.
  992. let eventStack = column.addStack()
  993. eventStack.layoutVertically()
  994. const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200
  995. eventStack.url = 'calshow:' + todaySeconds
  996.  
  997. // If there are no events and we have a message, show it and return.
  998. if (!eventData.eventsAreVisible && localizedText.noEventMessage.length) {
  999. let message = provideText(localizedText.noEventMessage, eventStack, textFormat.noEvents)
  1000. eventStack.setPadding(padding, padding, padding, padding)
  1001. return
  1002. }
  1003.  
  1004. // If we're not showing the message, don't pad the event stack.
  1005. eventStack.setPadding(0, 0, 0, 0)
  1006.  
  1007. // Add each event to the stack.
  1008. var currentStack = eventStack
  1009. const futureEvents = eventData.futureEvents
  1010. for (let i = 0; i < futureEvents.length; i++) {
  1011.  
  1012. const event = futureEvents[i]
  1013. const bottomPadding = (padding-10 < 0) ? 0 : padding-10
  1014.  
  1015. // If it's the tomorrow label, change to the tomorrow stack.
  1016. if (event.isLabel) {
  1017. let tomorrowStack = column.addStack()
  1018. tomorrowStack.layoutVertically()
  1019. const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800
  1020. tomorrowStack.url = 'calshow:' + tomorrowSeconds
  1021. currentStack = tomorrowStack
  1022.  
  1023. // Mimic the formatting of an event title, mostly.
  1024. const eventLabelStack = align(currentStack)
  1025. const eventLabel = provideText(event.title, eventLabelStack, textFormat.eventLabel)
  1026. eventLabelStack.setPadding(padding, padding, padding, padding)
  1027. continue
  1028. }
  1029.  
  1030. const titleStack = align(currentStack)
  1031. titleStack.layoutHorizontally()
  1032. const showCalendarColor = eventSettings.showCalendarColor
  1033. const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle"
  1034.  
  1035. // If we're showing a color, and it's not shown on the right, add it to the left.
  1036. if (showCalendarColor.length && !showCalendarColor.includes("right")) {
  1037. let colorItemText = provideTextSymbol(colorShape) + " "
  1038. let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle)
  1039. colorItem.textColor = event.calendar.color
  1040. }
  1041.  
  1042. const title = provideText(event.title.trim(), titleStack, textFormat.eventTitle)
  1043. titleStack.setPadding(padding, padding, event.isAllDay ? padding : padding/5, padding)
  1044.  
  1045. // If we're showing a color on the right, show it.
  1046. if (showCalendarColor.length && showCalendarColor.includes("right")) {
  1047. let colorItemText = " " + provideTextSymbol(colorShape)
  1048. let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle)
  1049. colorItem.textColor = event.calendar.color
  1050. }
  1051.  
  1052. // If there are too many events, limit the line height.
  1053. if (futureEvents.length >= 3) { title.lineLimit = 1 }
  1054.  
  1055. // If it's an all-day event, we don't need a time.
  1056. if (event.isAllDay) { continue }
  1057.  
  1058. // Format the time information.
  1059. let timeText = formatTime(event.startDate)
  1060.  
  1061. // If we show the length as time, add an en dash and the time.
  1062. if (eventSettings.showEventLength == "time") {
  1063. timeText += "–" + formatTime(event.endDate)
  1064.  
  1065. // If we should it as a duration, add the minutes.
  1066. } else if (eventSettings.showEventLength == "duration") {
  1067. const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60)
  1068. const hours = Math.floor(duration/60)
  1069. const minutes = Math.floor(duration % 60)
  1070. const hourText = hours>0 ? hours + localizedText.durationHour : ""
  1071. const minuteText = minutes>0 ? minutes + localizedText.durationMinute : ""
  1072. const showSpace = hourText.length && minuteText.length
  1073. timeText += " \u2022 " + hourText + (showSpace ? " " : "") + minuteText
  1074. }
  1075.  
  1076. const timeStack = align(currentStack)
  1077. const time = provideText(timeText, timeStack, textFormat.eventTime)
  1078. timeStack.setPadding(0, padding, padding, padding)
  1079. }
  1080. }
  1081.  
  1082. // Display the current weather.
  1083. async function current(column) {
  1084.  
  1085. // Requirements: weather and sunrise
  1086. if (!weatherData) { await setupWeather() }
  1087. if (!sunData) { await setupSunrise() }
  1088.  
  1089. // Set up the current weather stack.
  1090. let currentWeatherStack = column.addStack()
  1091. currentWeatherStack.layoutVertically()
  1092. currentWeatherStack.setPadding(0, 0, 0, 0)
  1093. currentWeatherStack.url = "https://weather.com/weather/today/l/" + locationData.latitude + "," + locationData.longitude
  1094.  
  1095. // If we're showing the location, add it.
  1096. if (weatherSettings.showLocation) {
  1097. let locationTextStack = align(currentWeatherStack)
  1098. let locationText = provideText(locationData.locality, locationTextStack, textFormat.smallTemp)
  1099. locationTextStack.setPadding(10, padding, padding, padding, padding)
  1100. }
  1101.  
  1102. // Show the current condition symbol.
  1103. let mainConditionStack = align(currentWeatherStack)
  1104. let mainCondition = mainConditionStack.addImage(provideConditionSymbol(weatherData.currentCondition,isNight(currentDate)))
  1105. mainCondition.imageSize = new Size(22,22)
  1106. tintIcon(mainCondition, textFormat.largeTemp)
  1107. mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : padding, padding, 0, padding)
  1108.  
  1109. // If we're showing the description, add it.
  1110. if (weatherSettings.showCondition) {
  1111. let conditionTextStack = align(currentWeatherStack)
  1112. let conditionText = provideText(weatherData.currentDescription, conditionTextStack, textFormat.smallTemp)
  1113. conditionTextStack.setPadding(padding, padding, 0, padding)
  1114. }
  1115.  
  1116. // Show the current temperature.
  1117. const tempStack = align(currentWeatherStack)
  1118. tempStack.setPadding(10, padding, 0, padding)
  1119. const tempText = Math.round(weatherData.currentTemp) + "°"
  1120. const temp = provideText(tempText, tempStack, textFormat.largeTemp)
  1121.  
  1122. // If we're not showing the high and low, end it here.
  1123. if (!weatherSettings.showHighLow) { return }
  1124.  
  1125. // Show the temp bar and high/low values.
  1126. let tempBarStack = align(currentWeatherStack)
  1127. tempBarStack.layoutVertically()
  1128. tempBarStack.setPadding(0, padding, padding, padding)
  1129.  
  1130. let tempBar = drawTempBar()
  1131. let tempBarImage = tempBarStack.addImage(tempBar)
  1132. tempBarImage.size = new Size(50,0)
  1133.  
  1134. tempBarStack.addSpacer(1)
  1135.  
  1136. let highLowStack = tempBarStack.addStack()
  1137. highLowStack.layoutHorizontally()
  1138.  
  1139. const mainLowText = Math.round(weatherData.todayLow).toString()
  1140. const mainLow = provideText(mainLowText, highLowStack, textFormat.tinyTemp)
  1141. highLowStack.addSpacer()
  1142. const mainHighText = Math.round(weatherData.todayHigh).toString()
  1143. const mainHigh = provideText(mainHighText, highLowStack, textFormat.tinyTemp)
  1144.  
  1145. tempBarStack.size = new Size(60,30)
  1146. }
  1147.  
  1148. // Display upcoming weather.
  1149. async function future(column) {
  1150.  
  1151. // Requirements: weather and sunrise
  1152. if (!weatherData) { await setupWeather() }
  1153. if (!sunData) { await setupSunrise() }
  1154.  
  1155. // Set up the future weather stack.
  1156. let futureWeatherStack = column.addStack()
  1157. futureWeatherStack.layoutVertically()
  1158. futureWeatherStack.setPadding(0, 0, 0, 0)
  1159. futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + locationData.latitude + "," + locationData.longitude
  1160.  
  1161. // Determine if we should show the next hour.
  1162. const showNextHour = (currentDate.getHours() < weatherSettings.tomorrowShownAtHour)
  1163.  
  1164. // Set the label value.
  1165. const subLabelStack = align(futureWeatherStack)
  1166. const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel
  1167. const subLabel = provideText(subLabelText, subLabelStack, textFormat.smallTemp)
  1168. subLabelStack.setPadding(0, padding, padding/2, padding)
  1169.  
  1170. // Set up the sub condition stack.
  1171. let subConditionStack = align(futureWeatherStack)
  1172. subConditionStack.layoutHorizontally()
  1173. subConditionStack.centerAlignContent()
  1174. subConditionStack.setPadding(0, padding, padding, padding)
  1175.  
  1176. // Determine if it will be night in the next hour.
  1177. var nightCondition
  1178. if (showNextHour) {
  1179. const addHour = currentDate.getTime() + (60*60*1000)
  1180. const newDate = new Date(addHour)
  1181. nightCondition = isNight(newDate)
  1182. } else {
  1183. nightCondition = false
  1184. }
  1185.  
  1186. let subCondition = subConditionStack.addImage(provideConditionSymbol(showNextHour ? weatherData.nextHourCondition : weatherData.tomorrowCondition,nightCondition))
  1187. const subConditionSize = showNextHour ? 14 : 18
  1188. subCondition.imageSize = new Size(subConditionSize, subConditionSize)
  1189. tintIcon(subCondition, textFormat.tinyTemp)
  1190. subConditionStack.addSpacer(5)
  1191.  
  1192. // The next part of the display changes significantly for next hour vs tomorrow.
  1193. if (showNextHour) {
  1194. const subTempText = Math.round(weatherData.nextHourTemp) + "°"
  1195. const subTemp = provideText(subTempText, subConditionStack, textFormat.tinyTemp)
  1196.  
  1197. } else {
  1198. let tomorrowLine = subConditionStack.addImage(drawVerticalLine(new Color(textFormat.tinyTemp.color || textFormat.defaultText.color, 0.5), 20))
  1199. tomorrowLine.imageSize = new Size(3,28)
  1200. subConditionStack.addSpacer(5)
  1201. let tomorrowStack = subConditionStack.addStack()
  1202. tomorrowStack.layoutVertically()
  1203.  
  1204. const tomorrowHighText = Math.round(weatherData.tomorrowHigh) + ""
  1205. const tomorrowHigh = provideText(tomorrowHighText, tomorrowStack, textFormat.tinyTemp)
  1206. tomorrowStack.addSpacer(4)
  1207. const tomorrowLowText = Math.round(weatherData.tomorrowLow) + ""
  1208. const tomorrowLow = provideText(tomorrowLowText, tomorrowStack, textFormat.tinyTemp)
  1209. }
  1210. }
  1211.  
  1212. // Return a text-creation function.
  1213. function text(input = null) {
  1214.  
  1215. function displayText(column) {
  1216.  
  1217. // Don't do anything if the input is blank.
  1218. if (!input || input == "") { return }
  1219.  
  1220. // Otherwise, add the text.
  1221. const textStack = align(column)
  1222. textStack.setPadding(padding, padding, padding, padding)
  1223. const textDisplay = provideText(input, textStack, textFormat.customText)
  1224. }
  1225. return displayText
  1226. }
  1227.  
  1228. // Add a battery element to the widget; consisting of a battery icon and percentage.
  1229. async function battery(column) {
  1230.  
  1231. // Get battery level via Scriptable function and format it in a convenient way
  1232. function getBatteryLevel() {
  1233.  
  1234. const batteryLevel = Device.batteryLevel()
  1235. const batteryPercentage = `${Math.round(batteryLevel * 100)}%`
  1236.  
  1237. return batteryPercentage
  1238. }
  1239.  
  1240. const batteryLevel = Device.batteryLevel()
  1241.  
  1242. // Set up the battery level item
  1243. let batteryStack = align(column)
  1244. batteryStack.layoutHorizontally()
  1245. batteryStack.centerAlignContent()
  1246.  
  1247. let batteryIcon = batteryStack.addImage(provideBatteryIcon())
  1248. batteryIcon.imageSize = new Size(30,30)
  1249. batteryIcon.tintColor = Color.white()
  1250.  
  1251. // Change the battery icon to yellow if battery level is <= 40 to match system behavior
  1252. if ( Math.round(batteryLevel * 100) > 40 || Device.isCharging() ) {
  1253.  
  1254. tintIcon(batteryIcon, textFormat.battery)
  1255.  
  1256. } else {
  1257.  
  1258. batteryIcon.tintColor = Color.yellow()
  1259.  
  1260. }
  1261.  
  1262. batteryStack.addSpacer(padding * 1.4)
  1263.  
  1264. // Display the battery status
  1265. let batteryInfo = provideText(getBatteryLevel(), batteryStack, textFormat.battery)
  1266.  
  1267. batteryStack.setPadding(padding/2, padding, padding/2, padding)
  1268.  
  1269. }
  1270.  
  1271. // Show the sunrise or sunset time.
  1272. async function sunrise(column) {
  1273.  
  1274. // Requirements: sunrise
  1275. if (!sunData) { await setupSunrise() }
  1276.  
  1277. const sunrise = sunData.sunrise
  1278. const sunset = sunData.sunset
  1279. const tomorrow = sunData.tomorrow
  1280. const current = currentDate.getTime()
  1281.  
  1282. const showWithin = sunriseSettings.showWithin
  1283. const closeToSunrise = closeTo(sunrise) <= showWithin
  1284. const closeToSunset = closeTo(sunset) <= showWithin
  1285.  
  1286. // If we only show sometimes and we're not close, return.
  1287. if (showWithin > 0 && !closeToSunrise && !closeToSunset) { return }
  1288.  
  1289. // Otherwise, determine which time to show.
  1290. let timeToShow, symbolName
  1291. const halfHour = 30 * 60 * 1000
  1292.  
  1293. // If we're between sunrise and sunset, show the sunset.
  1294. if (current > sunrise + halfHour && current < sunset + halfHour) {
  1295. symbolName = "sunset.fill"
  1296. timeToShow = sunset
  1297. }
  1298.  
  1299. // Otherwise, show a sunrise.
  1300. else {
  1301. symbolName = "sunrise.fill"
  1302. timeToShow = current > sunset ? tomorrow : sunrise
  1303. }
  1304.  
  1305. // Set up the stack.
  1306. const sunriseStack = align(column)
  1307. sunriseStack.setPadding(padding/2, padding, padding/2, padding)
  1308. sunriseStack.layoutHorizontally()
  1309. sunriseStack.centerAlignContent()
  1310.  
  1311. sunriseStack.addSpacer(padding * 1.0)
  1312.  
  1313. // Add the correct symbol.
  1314. const symbol = sunriseStack.addImage(SFSymbol.named(symbolName).image)
  1315. symbol.imageSize = new Size(22,22)
  1316. tintIcon(symbol, textFormat.sunrise)
  1317.  
  1318. sunriseStack.addSpacer(padding)
  1319.  
  1320. // Add the time.
  1321. const timeText = formatTime(new Date(timeToShow))
  1322. const time = provideText(timeText, sunriseStack, textFormat.sunrise)
  1323. }
  1324.  
  1325. // Allow for either term to be used.
  1326. async function sunset(column) {
  1327. return await sunrise(column)
  1328. }
  1329.  
  1330. /*
  1331. * HELPER FUNCTIONS
  1332. * These functions perform duties for other functions.
  1333. * ===================================================
  1334. */
  1335.  
  1336. // Tints icons if needed.
  1337. function tintIcon(icon,format) {
  1338. if (!tintIcons) { return }
  1339. icon.tintColor = new Color(format.color || textFormat.defaultText.color)
  1340. }
  1341.  
  1342. // Determines if the provided date is at night.
  1343. function isNight(dateInput) {
  1344. const timeValue = dateInput.getTime()
  1345. return (timeValue < sunData.sunrise) || (timeValue > sunData.sunset)
  1346. }
  1347.  
  1348. // Determines if two dates occur on the same day
  1349. function sameDay(d1, d2) {
  1350. return d1.getFullYear() === d2.getFullYear() &&
  1351. d1.getMonth() === d2.getMonth() &&
  1352. d1.getDate() === d2.getDate()
  1353. }
  1354.  
  1355. // Returns the number of minutes between now and the provided date.
  1356. function closeTo(time) {
  1357. return Math.abs(currentDate.getTime() - time) / 60000
  1358. }
  1359.  
  1360. // Format the time for a Date input.
  1361. function formatTime(date) {
  1362. let df = new DateFormatter()
  1363. df.locale = locale
  1364. df.useNoDateStyle()
  1365. df.useShortTimeStyle()
  1366. return df.string(date)
  1367. }
  1368.  
  1369. // Provide a text symbol with the specified shape.
  1370. function provideTextSymbol(shape) {
  1371.  
  1372. // Rectangle character.
  1373. if (shape.startsWith("rect")) {
  1374. return "\u2759"
  1375. }
  1376. // Circle character.
  1377. if (shape == "circle") {
  1378. return "\u2B24"
  1379. }
  1380. // Default to the rectangle.
  1381. return "\u2759"
  1382. }
  1383.  
  1384. // Provide a battery SFSymbol with accurate level drawn on top of it.
  1385. function provideBatteryIcon() {
  1386.  
  1387. // If we're charging, show the charging icon.
  1388. if (Device.isCharging()) { return SFSymbol.named("battery.100.bolt").image }
  1389.  
  1390. // Set the size of the battery icon.
  1391. const batteryWidth = 87
  1392. const batteryHeight = 41
  1393.  
  1394. // Start our draw context.
  1395. let draw = new DrawContext()
  1396. draw.opaque = false
  1397. draw.respectScreenScale = true
  1398. draw.size = new Size(batteryWidth, batteryHeight)
  1399.  
  1400. // Draw the battery.
  1401. draw.drawImageInRect(SFSymbol.named("battery.0").image, new Rect(0, 0, batteryWidth, batteryHeight))
  1402.  
  1403. // Match the battery level values to the SFSymbol.
  1404. const x = batteryWidth*0.1525
  1405. const y = batteryHeight*0.247
  1406. const width = batteryWidth*0.602
  1407. const height = batteryHeight*0.505
  1408.  
  1409. // Prevent unreadable icons.
  1410. let level = Device.batteryLevel()
  1411. if (level < 0.05) { level = 0.05 }
  1412.  
  1413. // Determine the width and radius of the battery level.
  1414. const current = width * level
  1415. let radius = height/6.5
  1416.  
  1417. // When it gets low, adjust the radius to match.
  1418. if (current < (radius * 2)) { radius = current / 2 }
  1419.  
  1420. // Make the path for the battery level.
  1421. let barPath = new Path()
  1422. barPath.addRoundedRect(new Rect(x, y, current, height), radius, radius)
  1423. draw.addPath(barPath)
  1424. const color = tintIcons ? (textFormat.battery.color || textFormat.defaultText.color) : "000000"
  1425. draw.setFillColor(new Color(color))
  1426. draw.fillPath()
  1427. return draw.getImage()
  1428. }
  1429.  
  1430. // Provide a symbol based on the condition.
  1431. function provideConditionSymbol(cond,night) {
  1432.  
  1433. // Define our symbol equivalencies.
  1434. let symbols = {
  1435.  
  1436. // Thunderstorm
  1437. "2": function() { return "cloud.bolt.rain.fill" },
  1438.  
  1439. // Drizzle
  1440. "3": function() { return "cloud.drizzle.fill" },
  1441.  
  1442. // Rain
  1443. "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" },
  1444.  
  1445. // Snow
  1446. "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" },
  1447.  
  1448. // Atmosphere
  1449. "7": function() {
  1450. if (cond == 781) { return "tornado" }
  1451. if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
  1452. return night ? "cloud.fog.fill" : "sun.haze.fill"
  1453. },
  1454.  
  1455. // Clear and clouds
  1456. "8": function() {
  1457. if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" }
  1458. if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
  1459. return "cloud.fill"
  1460. }
  1461. }
  1462.  
  1463. // Find out the first digit.
  1464. let conditionDigit = Math.floor(cond / 100)
  1465.  
  1466. // Get the symbol.
  1467. return SFSymbol.named(symbols[conditionDigit]()).image
  1468. }
  1469.  
  1470. // Provide a font based on the input.
  1471. function provideFont(fontName, fontSize) {
  1472. const fontGenerator = {
  1473. "ultralight": function() { return Font.ultraLightSystemFont(fontSize) },
  1474. "light": function() { return Font.lightSystemFont(fontSize) },
  1475. "regular": function() { return Font.regularSystemFont(fontSize) },
  1476. "medium": function() { return Font.mediumSystemFont(fontSize) },
  1477. "semibold": function() { return Font.semiboldSystemFont(fontSize) },
  1478. "bold": function() { return Font.boldSystemFont(fontSize) },
  1479. "heavy": function() { return Font.heavySystemFont(fontSize) },
  1480. "black": function() { return Font.blackSystemFont(fontSize) },
  1481. "italic": function() { return Font.italicSystemFont(fontSize) }
  1482. }
  1483.  
  1484. const systemFont = fontGenerator[fontName]
  1485. if (systemFont) { return systemFont() }
  1486. return new Font(fontName, fontSize)
  1487. }
  1488.  
  1489. // Add formatted text to a container.
  1490. function provideText(string, container, format) {
  1491. const textItem = container.addText(string)
  1492. const textFont = format.font || textFormat.defaultText.font
  1493. const textSize = format.size || textFormat.defaultText.size
  1494. const textColor = format.color || textFormat.defaultText.color
  1495.  
  1496. textItem.font = provideFont(textFont, textSize)
  1497. textItem.textColor = new Color(textColor)
  1498. return textItem
  1499. }
  1500.  
  1501. /*
  1502. * DRAWING FUNCTIONS
  1503. * These functions draw onto a canvas.
  1504. * ===================================
  1505. */
  1506.  
  1507. // Draw the vertical line in the tomorrow view.
  1508. function drawVerticalLine(color, height) {
  1509.  
  1510. const width = 2
  1511.  
  1512. let draw = new DrawContext()
  1513. draw.opaque = false
  1514. draw.respectScreenScale = true
  1515. draw.size = new Size(width,height)
  1516.  
  1517. let barPath = new Path()
  1518. const barHeight = height
  1519. barPath.addRoundedRect(new Rect(0, 0, width, height), width/2, width/2)
  1520. draw.addPath(barPath)
  1521. draw.setFillColor(color)
  1522. draw.fillPath()
  1523.  
  1524. return draw.getImage()
  1525. }
  1526.  
  1527. // Draw the temp bar.
  1528. function drawTempBar() {
  1529.  
  1530. // Set the size of the temp bar.
  1531. const tempBarWidth = 200
  1532. const tempBarHeight = 20
  1533.  
  1534. // Calculate the current percentage of the high-low range.
  1535. let percent = (weatherData.currentTemp - weatherData.todayLow) / (weatherData.todayHigh - weatherData.todayLow)
  1536.  
  1537. // If we're out of bounds, clip it.
  1538. if (percent < 0) {
  1539. percent = 0
  1540. } else if (percent > 1) {
  1541. percent = 1
  1542. }
  1543.  
  1544. // Determine the scaled x-value for the current temp.
  1545. const currPosition = (tempBarWidth - tempBarHeight) * percent
  1546.  
  1547. // Start our draw context.
  1548. let draw = new DrawContext()
  1549. draw.opaque = false
  1550. draw.respectScreenScale = true
  1551. draw.size = new Size(tempBarWidth, tempBarHeight)
  1552.  
  1553. // Make the path for the bar.
  1554. let barPath = new Path()
  1555. const barHeight = tempBarHeight - 10
  1556. barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2)
  1557. draw.addPath(barPath)
  1558.  
  1559. // Determine the color.
  1560. const barColor = textFormat.battery.color || textFormat.defaultText.color
  1561. draw.setFillColor(new Color(textFormat.tinyTemp.color || textFormat.defaultText.color, 0.5))
  1562. draw.fillPath()
  1563.  
  1564. // Make the path for the current temp indicator.
  1565. let currPath = new Path()
  1566. currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight))
  1567. draw.addPath(currPath)
  1568. draw.setFillColor(new Color(textFormat.tinyTemp.color || textFormat.defaultText.color, 1))
  1569. draw.fillPath()
  1570.  
  1571. return draw.getImage()
  1572. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement