forked from mzeryck/Weather-Cal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathweather-cal.js
922 lines (717 loc) · 28.1 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
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: magic;
/*
* SETUP
* Use this section to set up the widget.
* ======================================
*/
// Get a free API key here: openweathermap.org/appid
const apiKey = ""
// Set to true for fixed location, false to update location as you move around
const lockLocation = true
// Set to imperial for Fahrenheit, or metric for Celsius
const units = "imperial"
// 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 and run the script once to update the image manually.
const forceImageUpdate = false
// Set which calendars for which to show events.
// Empty array means all calendars.
// Example:
// const selectCalendars = ["Main", "Work", "Birthdays"]
const selectCalendars = [] // Default: All calendars
// Set the two-letter locale code for the date and weather formatting.
const locale = "en"
// You can change the language or wording of any other 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"
// The text shown in an events item when no events remain.
// Change to blank "" if you don't want to show a message.
,noEventMessage: "Enjoy the rest of your day."
}
/*
* LAYOUT
* Decide what elements 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, 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,
end]}, {
// Settings for the right column.
width: 100,
items: [
left,
current,
space,
future,
end]}]
/*
* FORMATTING
* Choose how each element is displayed.
* =====================================
*/
// EVENTS
// ======
// How many events to show.
const numberOfEvents = 3
// Show all-day events.
const showAllDay = true
// Show tomorrow's events.
const showTomorrow = true
// Can be blank "" or set to "duration" or "time" to display how long an event is.
const showEventLength = "duration"
// WEATHER
// =======
// Show today's high and low temperatures.
const showHighLow = true
// Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it.
const tomorrowShownAtHour = 20
// DATE
// ====
// If set to true, date will become smaller when events are displayed.
const dynamicDateSize = true
// If the date is not dynamic, should it be large or small?
const staticDateSize = "large"
// Determine the date format for each element. See docs.scriptable.app/dateformatter
const smallDateFormat = "EEEE, MMMM d"
const largeDateLineOne = "EEEE,"
const largeDateLineTwo = "MMMM d"
// In this section, set the font, size, and color. 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" },
eventTitle: { size: 14, color: "", font: "semibold" },
eventTime: { size: 14, color: "ffffffcc", font: "" },
largeTemp: { size: 34, color: "", font: "light" },
smallTemp: { size: 14, color: "", font: "" },
tinyTemp: { size: 12, color: "", font: "" },
customText: { size: 14, color: "", font: "" }
}
/*
* WIDGET CODE
* Be more careful editing this section.
* =====================================
*/
// Set up the date and event information.
const currentDate = new Date()
const todayEventsAll = await CalendarEvent.today([])
const tomorrowEventsAll = await CalendarEvent.tomorrow([])
const todayEvents = selectCalendars.lenght>0 ? todayEventsAll.filter(inSelectCalendars) : todayEventsAll
const tomorrowEvents = selectCalendars.lenght>0 ? tomorrowEventsAll.filter(inSelectCalendars) : tomorrowEventsAll
const futureEvents = enumerateEvents()
const eventsAreVisible = (futureEvents.length > 0) && (numberOfEvents > 0)
// Set up the file manager.
const files = FileManager.local()
// Set up the location logic.
const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-location")
var latitude, longitude
// If we're locking our location and it's saved already, read from the file.
if (lockLocation && files.fileExists(locationPath)) {
const locationStr = files.readString(locationPath).split(",")
latitude = locationStr[0]
longitude = locationStr[1]
// Otherwise, get the location from the system.
} else {
const location = await Location.current()
latitude = location.latitude
longitude = location.longitude
files.writeString(locationPath, latitude + "," + longitude)
}
// 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 data
// 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)
data = JSON.parse(cache)
// Otherwise, use the API to get new weather data.
} else {
const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + latitude + "&lon=" + longitude + "&exclude=minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + apiKey
data = await new Request(weatherReq).loadJSON()
files.writeString(cachePath, JSON.stringify(data))
}
// Store the weather values.
const currentTemp = data.current.temp
const currentCondition = data.current.weather[0].id
const todayHigh = data.daily[0].temp.max
const todayLow = data.daily[0].temp.min
const nextHourTemp = data.hourly[1].temp
const nextHourCondition = data.hourly[1].weather[0].id
const tomorrowHigh = data.daily[1].temp.max
const tomorrowLow = data.daily[1].temp.min
const tomorrowCondition = data.daily[1].weather[0].id
// 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
var sunData
// If cache exists and it was created today, use cached data.
if (sunCacheExists && sameDay(currentDate, sunCacheDate)) {
const sunCache = files.readString(sunCachePath)
sunData = JSON.parse(sunCache)
// Otherwise, use the API to get sunrise and sunset times.
} else {
const sunReq = "https://api.sunrise-sunset.org/json?lat=" + latitude + "&lng=" + longitude + "&formatted=0&date=" + currentDate.getFullYear() + "-" + (currentDate.getMonth()+1) + "-" + currentDate.getDate()
sunData = await new Request(sunReq).loadJSON()
files.writeString(sunCachePath, JSON.stringify(sunData))
}
// Store the timing values.
const sunrise = new Date(sunData.results.sunrise).getTime()
const sunset = new Date(sunData.results.sunset).getTime()
const utcTime = currentDate.getTime()
/*
* COLUMNS AND PADDING
* ===================
*/
// 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 ? 5 : 0, 0, x == columns.length-1 ? 5 : 0)
columnStack.size = new Size(column.width,0)
// Add the items to the column.
for (var i = 0; i < column.items.length; i++) {
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)
const createdToday = exists ? sameDay(files.modificationDate(path),currentDate) : false
// If it exists and updates aren't being forced, use the cache.
if (exists && !forceImageUpdate) {
widget.backgroundImage = files.readImage(path)
// If it's missing or forced to update...
} else if (!exists || forceImageUpdate) {
// ... just use a gray background if we're in the widget.
if (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 = getGradientSettings()
gradient.colors = gradientSettings.color()
gradient.locations = gradientSettings.position()
widget.backgroundGradient = gradient
}
Script.setWidget(widget)
if (widgetPreview == "small") { widget.presentSmall() }
else if (widgetPreview == "medium") { widget.presentMedium() }
else if (widgetPreview == "large") { widget.presentLarge() }
Script.complete()
/*
* IMAGES AND FORMATTING
* =====================
*/
// Get the gradient settings for each time of day.
function getGradientSettings() {
let gradient = {
"dawn": {
"color": function() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] },
"position": function() { return [0, 0.5, 1] }
},
"sunrise": {
"color": function() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] },
"position": function() { return [0, 0.8, 1.5] }
},
"midday": {
"color": function() { return [new Color("3a8cc1"), new Color("90c0df")] },
"position": function() { return [0, 1] }
},
"noon": {
"color": function() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] },
"position": function() { return [-0.2, 0.2, 1.5] }
},
"sunset": {
"color": function() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] },
"position": function() { return [0.1, 0.9, 1.2] }
},
"twilight": {
"color": function() { return [new Color("021033"), new Color("16296b"), new Color("414791")] },
"position": function() { return [0, 0.5, 1] }
},
"night": {
"color": function() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] },
"position": function() { return [-0.5, 0.2, 0.5, 1] }
}
}
function closeTo(time,mins) {
return Math.abs(utcTime - time) < (mins * 60000)
}
// 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
}
// Provide a symbol based on the condition.
function provideSymbol(cond,night) {
// Define our symbol equivalencies.
let symbols = {
// Thunderstorm
"2": function() { return "cloud.bolt.rain.fill" },
// Drizzle
"3": function() { return "cloud.drizzle.fill" },
// Rain
"5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" },
// Snow
"6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" },
// Atmosphere
"7": function() {
if (cond == 781) { return "tornado" }
if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
return night ? "cloud.fog.fill" : "sun.haze.fill"
},
// Clear and clouds
"8": function() {
if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" }
if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
return "cloud.fill"
}
}
// Find out the first digit.
let conditionDigit = Math.floor(cond / 100)
// Get the symbol.
return SFSymbol.named(symbols[conditionDigit]()).image
}
// Provide a font based on the input.
function provideFont(fontName, fontSize) {
const fontGenerator = {
"ultralight": function() { return Font.ultraLightSystemFont(fontSize) },
"light": function() { return Font.lightSystemFont(fontSize) },
"regular": function() { return Font.regularSystemFont(fontSize) },
"medium": function() { return Font.mediumSystemFont(fontSize) },
"semibold": function() { return Font.semiboldSystemFont(fontSize) },
"bold": function() { return Font.boldSystemFont(fontSize) },
"heavy": function() { return Font.heavySystemFont(fontSize) },
"black": function() { return Font.blackSystemFont(fontSize) },
"italic": function() { return Font.italicSystemFont(fontSize) }
}
const systemFont = fontGenerator[fontName]
if (systemFont) { return systemFont() }
return new Font(fontName, fontSize)
}
// Format text based on the settings.
function formatText(textItem, format) {
const textFont = format.font || textFormat.defaultText.font
const textSize = format.size || textFormat.defaultText.size
const textColor = format.color || textFormat.defaultText.color
textItem.font = provideFont(textFont, textSize)
textItem.textColor = new Color(textColor)
}
/*
* HELPER FUNCTIONS
* ================
*/
// Find future events that aren't all day and aren't canceled
function enumerateEvents() {
// Function to determine if an event should be shown.
function shouldShowEvent(event) {
// 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 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.
let shownEvents = 0
let returnedEvents = []
for (const event of todayEvents) {
if (shownEvents == numberOfEvents) { break }
if (shouldShowEvent(event)) {
returnedEvents.push(event)
shownEvents++
}
}
// If there's room and we need to, show tomorrow's events.
let multipleTomorrowEvents = false
if (showTomorrow && shownEvents < numberOfEvents) {
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.
returnedEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isAllDay: true, isLabel: true })
multipleTomorrowEvents = true
}
// Show the tomorrow event and increment the counter.
returnedEvents.push(event)
shownEvents++
}
}
}
return returnedEvents
}
// Determines if the provided date is at night.
function isNight(dateInput) {
const timeValue = dateInput.getTime()
return (timeValue < sunrise) || (timeValue > sunset)
}
// Determines if two dates occur on the same day
function sameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
}
// Determines if an event is on a selected calendar
function inSelectCalendars(event) {
return selectCalendars.includes(event.calendar.title)
}
/*
* DRAWING FUNCTIONS
* =================
*/
// Draw the vertical line in the tomorrow view.
function drawVerticalLine() {
const w = 2
const h = 20
let draw = new DrawContext()
draw.opaque = false
draw.respectScreenScale = true
draw.size = new Size(w,h)
let barPath = new Path()
const barHeight = h
barPath.addRoundedRect(new Rect(0, 0, w, h), w/2, w/2)
draw.addPath(barPath)
draw.setFillColor(new Color("ffffff", 0.5))
draw.fillPath()
return draw.getImage()
}
// Draw the temp bar.
function drawTempBar() {
// Set the size of the temp bar.
const tempBarWidth = 200
const tempBarHeight = 20
// Calculate the current percentage of the high-low range.
let percent = (currentTemp - todayLow) / (todayHigh - todayLow)
// If we're out of bounds, clip it.
if (percent < 0) {
percent = 0
} else if (percent > 1) {
percent = 1
}
// Determine the scaled x-value for the current temp.
const currPosition = (tempBarWidth - tempBarHeight) * percent
// Start our draw context.
let draw = new DrawContext()
draw.opaque = false
draw.respectScreenScale = true
draw.size = new Size(tempBarWidth, tempBarHeight)
// Make the path for the bar.
let barPath = new Path()
const barHeight = tempBarHeight - 10
barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2)
draw.addPath(barPath)
draw.setFillColor(new Color("ffffff", 0.5))
draw.fillPath()
// Make the path for the current temp indicator.
let currPath = new Path()
currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight))
draw.addPath(currPath)
draw.setFillColor(new Color("ffffff", 1))
draw.fillPath()
return draw.getImage()
}
/*
* ELEMENTS 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
}
// Display the date on the widget.
function date(column) {
// 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 ((dynamicDateSize && eventsAreVisible) || staticDateSize == "small") {
let dateStack = align(column)
dateStack.setPadding(10, 10, 10, 10)
df.dateFormat = smallDateFormat
let dateText = dateStack.addText(df.string(currentDate))
formatText(dateText, textFormat.smallDate)
// Otherwise, show the large date.
} else {
let dateOneStack = align(column)
df.dateFormat = largeDateLineOne
let dateOne = dateOneStack.addText(df.string(currentDate))
formatText(dateOne, textFormat.largeDate1)
dateOneStack.setPadding(10, 10, 0, 10)
let dateTwoStack = align(column)
df.dateFormat = largeDateLineTwo
let dateTwo = dateTwoStack.addText(df.string(currentDate))
formatText(dateTwo, textFormat.largeDate2)
dateTwoStack.setPadding(0, 10, 10, 10)
}
}
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 = greetingStack.addText(makeGreeting())
formatText(greeting, textFormat.greeting)
greetingStack.setPadding(10, 10, 10, 10)
}
// Display events on the widget.
function events(column) {
// If nothing should be displayed, just return.
if (!eventsAreVisible && !localizedText.noEventMessage.length) { 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, show the message and return.
if (!eventsAreVisible) {
let message = eventStack.addText(localizedText.noEventMessage)
formatText(message, textFormat.greeting)
eventStack.setPadding(10, 10, 10, 10)
return
}
// If we're not showing the message, don't pad the event stack.
eventStack.setPadding(0, 0, 0, 0)
var currentStack = eventStack
// Add each event to the stack.
for (let i = 0; i < futureEvents.length; i++) {
const event = futureEvents[i]
// 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
}
const titleStack = align(currentStack)
const title = titleStack.addText(event.title)
formatText(title, textFormat.eventTitle)
titleStack.setPadding(i==0 ? 10 : 5, 10, event.isAllDay ? 5 : 0, 10)
// 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 df = new DateFormatter()
df.useNoDateStyle()
df.useShortTimeStyle()
let timeText = df.string(event.startDate)
// If we show the length as time, add an en dash and the time.
if (showEventLength == "time") {
timeText += "–" + df.string(event.endDate)
// If we should it as a duration, add the minutes.
} else if (showEventLength == "duration") {
const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60)
timeText += " \u2022 " + Math.round(duration) + "m"
}
const timeStack = align(currentStack)
const time = timeStack.addText(timeText)
formatText(time, textFormat.eventTime)
timeStack.setPadding(0, 10, i==futureEvents.length-1 ? 10 : 5, 10)
}
}
// Display the current weather.
function current(column) {
// 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/" + latitude + "," + longitude
// Show the current condition symbol.
let mainConditionStack = align(currentWeatherStack)
let mainCondition = mainConditionStack.addImage(provideSymbol(currentCondition,isNight(currentDate)))
mainCondition.imageSize = new Size(22,22)
mainConditionStack.setPadding(10, 10, 0, 10)
// Show the current temperature.
let tempStack = align(currentWeatherStack)
let temp = tempStack.addText(Math.round(currentTemp) + "°")
tempStack.setPadding(0, 10, 0, 10)
formatText(temp, textFormat.largeTemp)
// If we're not showing the high and low, end it here.
if (!showHighLow) { return }
// Show the temp bar and high/low values.
let tempBarStack = align(currentWeatherStack)
tempBarStack.layoutVertically()
tempBarStack.setPadding(0, 10, 5, 10)
let tempBar = drawTempBar()
let tempBarImage = tempBarStack.addImage(tempBar)
tempBarImage.size = new Size(50,0)
tempBarStack.addSpacer(1)
let highLowStack = tempBarStack.addStack()
highLowStack.layoutHorizontally()
let mainLow = highLowStack.addText(Math.round(todayLow).toString())
highLowStack.addSpacer()
let mainHigh = highLowStack.addText(Math.round(todayHigh).toString())
formatText(mainHigh, textFormat.tinyTemp)
formatText(mainLow, textFormat.tinyTemp)
tempBarStack.size = new Size(70,30)
}
// Display upcoming weather.
function future(column) {
// 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/" + latitude + "," + longitude
// Determine if we should show the next hour.
const showNextHour = (currentDate.getHours() < tomorrowShownAtHour)
// Set the label value.
const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel
let subLabelStack = align(futureWeatherStack)
let subLabel = subLabelStack.addText(subLabelText)
formatText(subLabel, textFormat.smallTemp)
subLabelStack.setPadding(0, 10, 2, 10)
// Set up the sub condition stack.
let subConditionStack = align(futureWeatherStack)
subConditionStack.layoutHorizontally()
subConditionStack.centerAlignContent()
subConditionStack.setPadding(0, 10, 10, 10)
// Determine what condition to show.
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(provideSymbol(showNextHour ? nextHourCondition : 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) {
let subTemp = subConditionStack.addText(Math.round(nextHourTemp) + "°")
formatText(subTemp, textFormat.smallTemp)
} else {
let tomorrowLine = subConditionStack.addImage(drawVerticalLine())
tomorrowLine.imageSize = new Size(3,28)
subConditionStack.addSpacer(5)
let tomorrowStack = subConditionStack.addStack()
tomorrowStack.layoutVertically()
let tomorrowHighText = tomorrowStack.addText(Math.round(tomorrowHigh) + "")
tomorrowStack.addSpacer(4)
let tomorrowLowText = tomorrowStack.addText(Math.round(tomorrowLow) + "")
formatText(tomorrowHighText, textFormat.tinyTemp)
formatText(tomorrowLowText, textFormat.tinyTemp)
}
}
// Return a text-creation function.
function text(inputText) {
function displayText(column) {
let textStack = align(column)
textStack.setPadding(10, 10, 10, 10)
let textDisplay = textStack.addText(inputText)
formatText(textDisplay, textFormat.customText)
}
return displayText
}
/*
* MINI FUNCTIONS
* ==============
*/
// This function adds a space.
function space(column) { column.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 }
// This function doesn't need to do anything.
function end(x) { return }
Script.complete()