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