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