forked from mzeryck/Weather-Cal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
weather-cal.js
1240 lines (962 loc) · 40.2 KB
/
weather-cal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* SETUP
* Use this section to set up the widget.
* ======================================
*/
// To use weather, get a free API key at openweathermap.org/appid and paste it in between the quotation marks.
const apiKey = ""
// 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.
let locale = "en"
// Set to true for fixed location, false to update location as you move around
const lockLocation = true
// The size of the widget preview in the app.
const widgetPreview = "large"
// Set to true for an image background, false for no image.
const imageBackground = true
// Set to true to reset the widget's background image.
const forceImageUpdate = false
// Set the padding around each item. Default is 10.
const padding = 10
/*
* LAYOUT
* Decide what items to show on the widget.
* ========================================
*/
// Set the width of the column, or set to 0 for an automatic width.
// You can add items to the column:
// date, greeting, events, current, future, battery, text("Your text here")
// You can also add a left, center, or right to the list. Everything after it will be aligned that way.
// Make sure to always put a comma after each item.
const columns = [{
// Settings for the left column.
width: 0,
items: [
left,
greeting,
date,
events,
]}, {
// Settings for the right column.
width: 100,
items: [
left,
current,
space,
future,
]}]
/*
* ITEM SETTINGS
* Choose how each item is displayed.
* ==================================
*/
// DATE
// ====
const dateSettings = {
// If set to true, date will become smaller when events are displayed.
dynamicDateSize: true
// If the date is not dynamic, should it be large or small?
,staticDateSize: "large"
// Determine the date format for each date type. See docs.scriptable.app/dateformatter
,smallDateFormat: "EEEE, MMMM d"
,largeDateLineOne: "EEEE,"
,largeDateLineTwo: "MMMM d"
}
// EVENTS
// ======
const eventSettings = {
// How many events to show.
numberOfEvents: 3
// Show all-day events.
,showAllDay: true
// Show tomorrow's events.
,showTomorrow: true
// Can be blank "" or set to "duration" or "time" to display how long an event is.
,showEventLength: "duration"
// Set which calendars for which to show events. Empty [] means all calendars.
,selectCalendars: []
// Leave blank "" for no color, or specify shape (circle, rectangle) and/or side (left, right).
,showCalendarColor: "rectangle left"
// When no events remain, show a hard-coded "message", a "greeting", or "none".
,noEventBehavior: "message"
}
// SUNRISE
// =======
const sunriseSettings = {
// How many minutes before/after sunrise or sunset to show this element. 0 for always.
showWithin: 0
}
// WEATHER
// =======
const weatherSettings = {
// Set to imperial for Fahrenheit, or metric for Celsius
units: "imperial"
// Show the location of the current weather.
,showLocation: false
// Show the text description of the current conditions.
,showCondition: false
// Show today's high and low temperatures.
,showHighLow: true
// Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it.
,tomorrowShownAtHour: 20
}
/*
* TEXT
* Change the language and formatting of text displayed.
* =====================================================
*/
// You can change the language or wording of any text in the widget.
const localizedText = {
// The text shown if you add a greeting item to the layout.
nightGreeting: "Good night."
,morningGreeting: "Good morning."
,afternoonGreeting: "Good afternoon."
,eveningGreeting: "Good evening."
// The text shown if you add a future weather item to the layout, or tomorrow's events.
,nextHourLabel: "Next hour"
,tomorrowLabel: "Tomorrow"
// Shown when noEventBehavior is set to "message".
,noEventMessage: "Enjoy the rest of your day."
// The text shown after the hours and minutes of an event duration.
,durationMinute: "m"
,durationHour: "h"
}
// 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.
const textFormat = {
// Set the default font and color.
defaultText: { size: 14, color: "ffffff", font: "regular" },
// Any blank values will use the default.
smallDate: { size: 17, color: "", font: "semibold" },
largeDate1: { size: 30, color: "", font: "light" },
largeDate2: { size: 30, color: "", font: "light" },
greeting: { size: 30, color: "", font: "semibold" },
eventLabel: { size: 14, color: "", font: "semibold" },
eventTitle: { size: 14, color: "", font: "semibold" },
eventTime: { size: 14, color: "ffffffcc", font: "" },
noEvents: { size: 30, color: "", font: "semibold" },
largeTemp: { size: 34, color: "", font: "light" },
smallTemp: { size: 14, color: "", font: "" },
tinyTemp: { size: 12, color: "", font: "" },
customText: { size: 14, color: "", font: "" },
battery: { size: 18, color: "", font: "medium" },
sunrise: { size: 18, color: "", font: "medium" },
}
/*
* WIDGET CODE
* Be more careful editing this section.
* =====================================
*/
// Make sure we have a locale value.
if (locale == "" || locale == null) { locale = Device.locale() }
// Declare the data variables.
var eventData, locationData, sunData, weatherData
// Create global constants.
const currentDate = new Date()
const files = FileManager.local()
/*
* COLUMNS
* =======
*/
// Set up the widget and the main stack.
let widget = new ListWidget()
widget.setPadding(0, 0, 0, 0)
let mainStack = widget.addStack()
mainStack.layoutHorizontally()
mainStack.setPadding(0, 0, 0, 0)
// Set up alignment.
var currentAlignment = left
// Set up our columns.
for (var x = 0; x < columns.length; x++) {
let column = columns[x]
let columnStack = mainStack.addStack()
columnStack.layoutVertically()
// Only add padding on the first or last column.
columnStack.setPadding(0, x == 0 ? padding/2 : 0, 0, x == columns.length-1 ? padding/2 : 0)
columnStack.size = new Size(column.width,0)
// Add the items to the column.
for (var i = 0; i < column.items.length; i++) {
await column.items[i](columnStack)
}
}
/*
* BACKGROUND DISPLAY
* ==================
*/
// If it's an image background, display it.
if (imageBackground) {
// Determine if our image exists and when it was saved.
const path = files.joinPath(files.documentsDirectory(), "weather-cal-image")
const exists = files.fileExists(path)
// If it exists and an update isn't forced, use the cache.
if (exists && (config.runsInWidget || !forceImageUpdate)) {
widget.backgroundImage = files.readImage(path)
// If it's missing when running in the widget, use a gray background.
} else if (!exists && config.runsInWidget) {
widget.backgroundColor = Color.gray()
// But if we're running in app, prompt the user for the image.
} else {
const img = await Photos.fromLibrary()
widget.backgroundImage = img
files.writeImage(path, img)
}
// If it's not an image background, show the gradient.
} else {
let gradient = new LinearGradient()
let gradientSettings = await setupGradient()
gradient.colors = gradientSettings.color()
gradient.locations = gradientSettings.position()
widget.backgroundGradient = gradient
}
// Finish the widget and show a preview.
Script.setWidget(widget)
if (widgetPreview == "small") { widget.presentSmall() }
else if (widgetPreview == "medium") { widget.presentMedium() }
else if (widgetPreview == "large") { widget.presentLarge() }
Script.complete()
/*
* LAYOUT FUNCTIONS
* These functions manage spacing and alignment.
* =============================================
*/
// Create an aligned stack to add content to.
function align(column) {
// Add the containing stack to the column.
let alignmentStack = column.addStack()
alignmentStack.layoutHorizontally()
// Get the correct stack from the alignment function.
let returnStack = currentAlignment(alignmentStack)
returnStack.layoutVertically()
return returnStack
}
// Create a right-aligned stack.
function alignRight(alignmentStack) {
alignmentStack.addSpacer()
let returnStack = alignmentStack.addStack()
return returnStack
}
// Create a left-aligned stack.
function alignLeft(alignmentStack) {
let returnStack = alignmentStack.addStack()
alignmentStack.addSpacer()
return returnStack
}
// Create a center-aligned stack.
function alignCenter(alignmentStack) {
alignmentStack.addSpacer()
let returnStack = alignmentStack.addStack()
alignmentStack.addSpacer()
return returnStack
}
// This function adds a space, with an optional amount.
function space(input = null) {
// This function adds a spacer with the input width.
function spacer(column) {
// If the input is null or zero, add a flexible spacer.
if (!input || input == 0) { column.addSpacer() }
// Otherwise, add a space with the specified length.
else { column.addSpacer(input) }
}
// If there's no input or it's a number, it's being called in the column declaration.
if (!input || typeof input == "number") { return spacer }
// Otherwise, it's being called in the column generator.
else { input.addSpacer() }
}
// Change the current alignment to right.
function right(x) { currentAlignment = alignRight }
// Change the current alignment to left.
function left(x) { currentAlignment = alignLeft }
// Change the current alignment to center.
function center(x) { currentAlignment = alignCenter }
/*
* SETUP FUNCTIONS
* These functions prepare data needed for items.
* ==============================================
*/
// Set up the eventData object.
async function setupEvents() {
eventData = {}
const calendars = eventSettings.selectCalendars
const numberOfEvents = eventSettings.numberOfEvents
// Function to determine if an event should be shown.
function shouldShowEvent(event) {
// If events are filtered and the calendar isn't in the selected calendars, return false.
if (calendars.length && !calendars.includes(event.calendar.title)) { return false }
// Hack to remove canceled Office 365 events.
if (event.title.startsWith("Canceled:")) { return false }
// If it's an all-day event, only show if the setting is active.
if (event.isAllDay) { return eventSettings.showAllDay }
// Otherwise, return the event if it's in the future.
return (event.startDate.getTime() > currentDate.getTime())
}
// Determine which events to show, and how many.
const todayEvents = await CalendarEvent.today([])
let shownEvents = 0
let futureEvents = []
for (const event of todayEvents) {
if (shownEvents == numberOfEvents) { break }
if (shouldShowEvent(event)) {
futureEvents.push(event)
shownEvents++
}
}
// If there's room and we need to, show tomorrow's events.
let multipleTomorrowEvents = false
if (eventSettings.showTomorrow && shownEvents < numberOfEvents) {
const tomorrowEvents = await CalendarEvent.tomorrow([])
for (const event of tomorrowEvents) {
if (shownEvents == numberOfEvents) { break }
if (shouldShowEvent(event)) {
// Add the tomorrow label prior to the first tomorrow event.
if (!multipleTomorrowEvents) {
// The tomorrow label is pretending to be an event.
futureEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isLabel: true })
multipleTomorrowEvents = true
}
// Show the tomorrow event and increment the counter.
futureEvents.push(event)
shownEvents++
}
}
}
// Store the future events, and whether or not any events are displayed.
eventData.futureEvents = futureEvents
eventData.eventsAreVisible = (futureEvents.length > 0) && (eventSettings.numberOfEvents > 0)
}
// Set up the gradient for the widget background.
async function setupGradient() {
// Requirements: sunrise
if (!sunData) { await setupSunrise() }
let gradient = {
dawn: {
color() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] },
position() { return [0, 0.5, 1] },
},
sunrise: {
color() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] },
position() { return [0, 0.8, 1.5] },
},
midday: {
color() { return [new Color("3a8cc1"), new Color("90c0df")] },
position() { return [0, 1] },
},
noon: {
color() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] },
position() { return [-0.2, 0.2, 1.5] },
},
sunset: {
color() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] },
position() { return [0.1, 0.9, 1.2] },
},
twilight: {
color() { return [new Color("021033"), new Color("16296b"), new Color("414791")] },
position() { return [0, 0.5, 1] },
},
night: {
color() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] },
position() { return [-0.5, 0.2, 0.5, 1] },
},
}
const sunrise = sunData.sunrise
const sunset = sunData.sunset
// Use sunrise or sunset if we're within 30min of it.
if (closeTo(sunrise)<=15) { return gradient.sunrise }
if (closeTo(sunset)<=15) { return gradient.sunset }
// In the 30min before/after, use dawn/twilight.
if (closeTo(sunrise)<=45 && utcTime < sunrise) { return gradient.dawn }
if (closeTo(sunset)<=45 && utcTime > sunset) { return gradient.twilight }
// Otherwise, if it's night, return night.
if (isNight(currentDate)) { return gradient.night }
// If it's around noon, the sun is high in the sky.
if (currentDate.getHours() == 12) { return gradient.noon }
// Otherwise, return the "typical" theme.
return gradient.midday
}
// Set up the locationData object.
async function setupLocation() {
locationData = {}
const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-loc")
// If our location is unlocked or cache doesn't exist, ask iOS for location.
var readLocationFromFile = false
if (!lockLocation || !files.fileExists(locationPath)) {
try {
const location = await Location.current()
const geocode = await Location.reverseGeocode(location.latitude, location.longitude, locale)
locationData.latitude = location.latitude
locationData.longitude = location.longitude
locationData.locality = geocode[0].locality
files.writeString(locationPath, location.latitude + "|" + location.longitude + "|" + locationData.locality)
} catch(e) {
// If we fail in unlocked mode, read it from the cache.
if (!lockLocation) { readLocationFromFile = true }
// We can't recover if we fail on first run in locked mode.
else { return }
}
}
// If our location is locked or we need to read from file, do it.
if (lockLocation || readLocationFromFile) {
const locationStr = files.readString(locationPath).split("|")
locationData.latitude = locationStr[0]
locationData.longitude = locationStr[1]
locationData.locality = locationStr[2]
}
}
// Set up the sunData object.
async function setupSunrise() {
// Requirements: location
if (!locationData) { await setupLocation() }
// Set up the sunrise/sunset cache.
const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sun")
const sunCacheExists = files.fileExists(sunCachePath)
const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0
let sunDataRaw, afterSunset
// If cache exists and was created today, use cached data.
if (sunCacheExists && sameDay(currentDate, sunCacheDate)) {
const sunCache = files.readString(sunCachePath)
sunDataRaw = JSON.parse(sunCache)
// Determine if it's after sunset.
const sunsetDate = new Date(sunDataRaw.results.sunset)
afterSunset = currentDate.getTime() - sunsetDate.getTime() > (45 * 60 * 1000)
}
// If we don't have data yet, or we need to get tomorrow's data, get it from the server.
if (!sunDataRaw || afterSunset) {
let tomorrowDate = new Date()
tomorrowDate.setDate(currentDate.getDate() + 1)
const dateToUse = afterSunset ? tomorrowDate : currentDate
const sunReq = "https://api.sunrise-sunset.org/json?lat=" + locationData.latitude + "&lng=" + locationData.longitude + "&formatted=0&date=" + dateToUse.getFullYear() + "-" + (dateToUse.getMonth()+1) + "-" + dateToUse.getDate()
sunDataRaw = await new Request(sunReq).loadJSON()
files.writeString(sunCachePath, JSON.stringify(sunDataRaw))
}
// Store the timing values.
sunData = {}
sunData.sunrise = new Date(sunDataRaw.results.sunrise).getTime()
sunData.sunset = new Date(sunDataRaw.results.sunset).getTime()
}
// Set up the weatherData object.
async function setupWeather() {
// Requirements: location
if (!locationData) { await setupLocation() }
// Set up the cache.
const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache")
const cacheExists = files.fileExists(cachePath)
const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
var weatherDataRaw
// If cache exists and it's been less than 60 seconds since last request, use cached data.
if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) {
const cache = files.readString(cachePath)
weatherDataRaw = JSON.parse(cache)
// Otherwise, use the API to get new weather data.
} else {
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
weatherDataRaw = await new Request(weatherReq).loadJSON()
files.writeString(cachePath, JSON.stringify(weatherDataRaw))
}
// Store the weather values.
weatherData = {}
weatherData.currentTemp = weatherDataRaw.current.temp
weatherData.currentCondition = weatherDataRaw.current.weather[0].id
weatherData.currentDescription = weatherDataRaw.current.weather[0].main
weatherData.todayHigh = weatherDataRaw.daily[0].temp.max
weatherData.todayLow = weatherDataRaw.daily[0].temp.min
weatherData.nextHourTemp = weatherDataRaw.hourly[1].temp
weatherData.nextHourCondition = weatherDataRaw.hourly[1].weather[0].id
weatherData.tomorrowHigh = weatherDataRaw.daily[1].temp.max
weatherData.tomorrowLow = weatherDataRaw.daily[1].temp.min
weatherData.tomorrowCondition = weatherDataRaw.daily[1].weather[0].id
}
/*
* WIDGET ITEMS
* These functions display items on the widget.
* ============================================
*/
// Display the date on the widget.
async function date(column) {
// Requirements: events (if dynamicDateSize is enabled)
if (!eventData && dateSettings.dynamicDateSize) { await setupEvents() }
// Set up the date formatter and set its locale.
let df = new DateFormatter()
df.locale = locale
// Show small if it's hard coded, or if it's dynamic and events are visible.
if (dateSettings.staticDateSize == "small" || (dateSettings.dynamicDateSize && eventData.eventsAreVisible)) {
let dateStack = align(column)
dateStack.setPadding(padding, padding, padding, padding)
df.dateFormat = dateSettings.smallDateFormat
let dateText = provideText(df.string(currentDate), dateStack, textFormat.smallDate)
// Otherwise, show the large date.
} else {
let dateOneStack = align(column)
df.dateFormat = dateSettings.largeDateLineOne
let dateOne = provideText(df.string(currentDate), dateOneStack, textFormat.largeDate1)
dateOneStack.setPadding(padding, padding, 0, padding)
let dateTwoStack = align(column)
df.dateFormat = dateSettings.largeDateLineTwo
let dateTwo = provideText(df.string(currentDate), dateTwoStack, textFormat.largeDate2)
dateTwoStack.setPadding(0, padding, padding, 10)
}
}
// Display a time-based greeting on the widget.
async function greeting(column) {
// This function makes a greeting based on the time of day.
function makeGreeting() {
const hour = currentDate.getHours()
if (hour < 5) { return localizedText.nightGreeting }
if (hour < 12) { return localizedText.morningGreeting }
if (hour-12 < 5) { return localizedText.afternoonGreeting }
if (hour-12 < 10) { return localizedText.eveningGreeting }
return localizedText.nightGreeting
}
// Set up the greeting.
let greetingStack = align(column)
let greeting = provideText(makeGreeting(), greetingStack, textFormat.greeting)
greetingStack.setPadding(padding, padding, padding, padding)
}
// Display events on the widget.
async function events(column) {
// Requirements: events
if (!eventData) { await setupEvents() }
// If no events are visible, figure out what to do.
if (!eventData.eventsAreVisible) {
const display = eventSettings.noEventBehavior
// If it's a greeting, let the greeting function handle it.
if (display == "greeting") { return await greeting(column) }
// If it's a message, get the localized text.
if (display == "message" && localizedText.noEventMessage.length) {
const messageStack = align(column)
messageStack.setPadding(padding, padding, padding, padding)
provideText(localizedText.noEventMessage, messageStack, textFormat.noEvents)
}
// Whether or not we displayed something, return here.
return
}
// Set up the event stack.
let eventStack = column.addStack()
eventStack.layoutVertically()
const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200
eventStack.url = 'calshow:' + todaySeconds
// If there are no events and we have a message, show it and return.
if (!eventData.eventsAreVisible && localizedText.noEventMessage.length) {
let message = provideText(localizedText.noEventMessage, eventStack, textFormat.noEvents)
eventStack.setPadding(padding, padding, padding, padding)
return
}
// If we're not showing the message, don't pad the event stack.
eventStack.setPadding(0, 0, 0, 0)
// Add each event to the stack.
var currentStack = eventStack
const futureEvents = eventData.futureEvents
for (let i = 0; i < futureEvents.length; i++) {
const event = futureEvents[i]
const bottomPadding = (padding-10 < 0) ? 0 : padding-10
// If it's the tomorrow label, change to the tomorrow stack.
if (event.isLabel) {
let tomorrowStack = column.addStack()
tomorrowStack.layoutVertically()
const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800
tomorrowStack.url = 'calshow:' + tomorrowSeconds
currentStack = tomorrowStack
// Mimic the formatting of an event title, mostly.
const eventLabelStack = align(currentStack)
const eventLabel = provideText(event.title, eventLabelStack, textFormat.eventLabel)
eventLabelStack.setPadding(i==0 ? padding : padding/2, padding, padding/2, padding)
continue
}
const titleStack = align(currentStack)
titleStack.layoutHorizontally()
const showCalendarColor = eventSettings.showCalendarColor
const colorShape = showCalendarColor.includes("circle") ? "circle" : "rectangle"
// If we're showing a color, and it's not shown on the right, add it to the left.
if (showCalendarColor.length && !showCalendarColor.includes("right")) {
let colorItemText = provideTextSymbol(colorShape) + " "
let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle)
colorItem.textColor = event.calendar.color
}
const title = provideText(event.title.trim(), titleStack, textFormat.eventTitle)
titleStack.setPadding(i==0 ? padding : padding/2, padding, event.isAllDay ? padding/2 : padding/10, padding)
// If we're showing a color on the right, show it.
if (showCalendarColor.length && showCalendarColor.includes("right")) {
let colorItemText = " " + provideTextSymbol(colorShape)
let colorItem = provideText(colorItemText, titleStack, textFormat.eventTitle)
colorItem.textColor = event.calendar.color
}
// If there are too many events, limit the line height.
if (futureEvents.length >= 3) { title.lineLimit = 1 }
// If it's an all-day event, we don't need a time.
if (event.isAllDay) { continue }
// Format the time information.
let timeText = formatTime(event.startDate)
// If we show the length as time, add an en dash and the time.
if (eventSettings.showEventLength == "time") {
timeText += "–" + formatTime(event.endDate)
// If we should it as a duration, add the minutes.
} else if (eventSettings.showEventLength == "duration") {
const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60)
const hours = Math.floor(duration/60)
const minutes = Math.floor(duration % 60)
const hourText = hours>0 ? hours + localizedText.durationHour : ""
const minuteText = minutes>0 ? minutes + localizedText.durationMinute : ""
const showSpace = hourText.length && minuteText.length
timeText += " \u2022 " + hourText + (showSpace ? " " : "") + minuteText
}
const timeStack = align(currentStack)
const time = provideText(timeText, timeStack, textFormat.eventTime)
timeStack.setPadding(0, padding, i==futureEvents.length-1 ? padding : padding/2, padding)
}
}
// Display the current weather.
async function current(column) {
// Requirements: weather and sunrise
if (!weatherData) { await setupWeather() }
if (!sunData) { await setupSunrise() }
// Set up the current weather stack.
let currentWeatherStack = column.addStack()
currentWeatherStack.layoutVertically()
currentWeatherStack.setPadding(0, 0, 0, 0)
currentWeatherStack.url = "https://weather.com/weather/today/l/" + locationData.latitude + "," + locationData.longitude
// If we're showing the location, add it.
if (weatherSettings.showLocation) {
let locationTextStack = align(currentWeatherStack)
let locationText = provideText(locationData.locality, locationTextStack, textFormat.smallTemp)
locationTextStack.setPadding(padding, padding, padding/2, padding)
}
// Show the current condition symbol.
let mainConditionStack = align(currentWeatherStack)
let mainCondition = mainConditionStack.addImage(provideConditionSymbol(weatherData.currentCondition,isNight(currentDate)))
mainCondition.imageSize = new Size(22,22)
mainConditionStack.setPadding(weatherSettings.showLocation ? 0 : padding, padding, 0, padding)
// If we're showing the description, add it.
if (weatherSettings.showCondition) {
let conditionTextStack = align(currentWeatherStack)
let conditionText = provideText(weatherData.currentDescription, conditionTextStack, textFormat.smallTemp)
conditionTextStack.setPadding(padding/2, padding, 0, padding)
}
// Show the current temperature.
const tempStack = align(currentWeatherStack)
tempStack.setPadding(0, padding, 0, padding)
const tempText = Math.round(weatherData.currentTemp) + "°"
const temp = provideText(tempText, tempStack, textFormat.largeTemp)
// If we're not showing the high and low, end it here.
if (!weatherSettings.showHighLow) { return }
// Show the temp bar and high/low values.
let tempBarStack = align(currentWeatherStack)
tempBarStack.layoutVertically()
tempBarStack.setPadding(0, padding, padding/2, padding)
let tempBar = drawTempBar()
let tempBarImage = tempBarStack.addImage(tempBar)
tempBarImage.size = new Size(50,0)
tempBarStack.addSpacer(1)
let highLowStack = tempBarStack.addStack()
highLowStack.layoutHorizontally()
const mainLowText = Math.round(weatherData.todayLow).toString()
const mainLow = provideText(mainLowText, highLowStack, textFormat.tinyTemp)
highLowStack.addSpacer()
const mainHighText = Math.round(weatherData.todayHigh).toString()
const mainHigh = provideText(mainHighText, highLowStack, textFormat.tinyTemp)
tempBarStack.size = new Size(70,30)
}
// Display upcoming weather.
async function future(column) {
// Requirements: weather and sunrise
if (!weatherData) { await setupWeather() }
if (!sunData) { await setupSunrise() }
// Set up the future weather stack.
let futureWeatherStack = column.addStack()
futureWeatherStack.layoutVertically()
futureWeatherStack.setPadding(0, 0, 0, 0)
futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + locationData.latitude + "," + locationData.longitude
// Determine if we should show the next hour.
const showNextHour = (currentDate.getHours() < weatherSettings.tomorrowShownAtHour)
// Set the label value.
const subLabelStack = align(futureWeatherStack)
const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel
const subLabel = provideText(subLabelText, subLabelStack, textFormat.smallTemp)
subLabelStack.setPadding(0, padding, padding/4, padding)
// Set up the sub condition stack.
let subConditionStack = align(futureWeatherStack)
subConditionStack.layoutHorizontally()
subConditionStack.centerAlignContent()
subConditionStack.setPadding(0, padding, padding, padding)
// Determine if it will be night in the next hour.
var nightCondition
if (showNextHour) {
const addHour = currentDate.getTime() + (60*60*1000)
const newDate = new Date(addHour)
nightCondition = isNight(newDate)
} else {
nightCondition = false
}
let subCondition = subConditionStack.addImage(provideConditionSymbol(showNextHour ? weatherData.nextHourCondition : weatherData.tomorrowCondition,nightCondition))
const subConditionSize = showNextHour ? 14 : 18
subCondition.imageSize = new Size(subConditionSize, subConditionSize)
subConditionStack.addSpacer(5)
// The next part of the display changes significantly for next hour vs tomorrow.
if (showNextHour) {
const subTempText = Math.round(weatherData.nextHourTemp) + "°"
const subTemp = provideText(subTempText, subConditionStack, textFormat.smallTemp)
} else {
let tomorrowLine = subConditionStack.addImage(drawVerticalLine(new Color("ffffff", 0.5), 20))
tomorrowLine.imageSize = new Size(3,28)
subConditionStack.addSpacer(5)
let tomorrowStack = subConditionStack.addStack()
tomorrowStack.layoutVertically()
const tomorrowHighText = Math.round(weatherData.tomorrowHigh) + ""
const tomorrowHigh = provideText(tomorrowHighText, tomorrowStack, textFormat.tinyTemp)
tomorrowStack.addSpacer(4)
const tomorrowLowText = Math.round(weatherData.tomorrowLow) + ""
const tomorrowLow = provideText(tomorrowLowText, tomorrowStack, textFormat.tinyTemp)
}
}
// Return a text-creation function.
function text(input = null) {
function displayText(column) {
// Don't do anything if the input is blank.
if (!input || input == "") { return }
// Otherwise, add the text.
const textStack = align(column)
textStack.setPadding(padding, padding, padding, padding)
const textDisplay = provideText(input, textStack, textFormat.customText)
}
return displayText
}
// Add a battery element to the widget; consisting of a battery icon and percentage.
async function battery(column) {
// Get battery level via Scriptable function and format it in a convenient way
function getBatteryLevel() {
const batteryLevel = Device.batteryLevel()
const batteryPercentage = `${Math.round(batteryLevel * 100)}%`
return batteryPercentage
}
const batteryLevel = Device.batteryLevel()
// Set up the battery level item
let batteryStack = align(column)
batteryStack.layoutHorizontally()
batteryStack.centerAlignContent()
let batteryIcon = batteryStack.addImage(provideBatteryIcon())
batteryIcon.imageSize = new Size(30,30)
// Change the battery icon to red if battery level is <= 20 to match system behavior
if ( Math.round(batteryLevel * 100) > 20 || Device.isCharging() ) {
batteryIcon.tintColor = new Color(textFormat.battery.color || textFormat.defaultText.color)
} else {
batteryIcon.tintColor = Color.red()
}
batteryStack.addSpacer(padding * 0.8)
// Display the battery status
let batteryInfo = provideText(getBatteryLevel(), batteryStack, textFormat.battery)
batteryStack.setPadding(padding, padding, padding, padding)
}
// Show the sunrise or sunset time.
async function sunrise(column) {
// Requirements: sunrise
if (!sunData) { await setupSunrise() }
const sunrise = sunData.sunrise
const sunset = sunData.sunset
const showWithin = sunriseSettings.showWithin
const closeToSunrise = closeTo(sunrise) <= showWithin
const closeToSunset = closeTo(sunset) <= showWithin
// If we only show sometimes and we're not close, return.
if (showWithin > 0 && !closeToSunrise && !closeToSunset) { return }
// Otherwise, determine which time to show.
const showSunrise = closeTo(sunrise) <= closeTo(sunset)
// Set up the stack.
const sunriseStack = align(column)
sunriseStack.setPadding(padding, padding, padding, padding)
sunriseStack.layoutHorizontally()
sunriseStack.centerAlignContent()
// Add the correct symbol.
const symbolName = showSunrise ? "sunrise.fill" : "sunset.fill"
const symbol = sunriseStack.addImage(SFSymbol.named(symbolName).image)
symbol.imageSize = new Size(22,22)
sunriseStack.addSpacer(padding * 0.8)
// Add the time.
const timeText = formatTime(showSunrise ? new Date(sunrise) : new Date(sunset))
const time = provideText(timeText, sunriseStack, textFormat.sunrise)