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