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