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