-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathMMM-ImmichSlideShow.js
More file actions
executable file
·1061 lines (975 loc) · 42 KB
/
Copy pathMMM-ImmichSlideShow.js
File metadata and controls
executable file
·1061 lines (975 loc) · 42 KB
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
/* global Module */
/* MMM-ImmichSlideShow.js
*
* Magic Mirror
* Module: MMM-ImmichSlideShow
*
* Magic Mirror By Michael Teeuw http://michaelteeuw.nl
* MIT Licensed.
*
* Module MMM-Slideshow By Darick Carpenter
* MIT Licensed.
*/
// const Log = console;
const LOG_PREFIX = 'MMM-ImmichSlideShow :: module :: ';
const MODE_MEMORY = 'memory';
const MODE_ALBUM = 'album';
const MODE_SEARCH = 'search';
const MODE_RANDOM = 'random';
const MODE_ANNIVERSARY = 'anniversary';
const DEFAULT_DATE_FORMAT = 'dddd MMMM D, YYYY HH:mm';
Module.register('MMM-ImmichSlideShow', {
// Min version of MM2 required
requiresVersion: "2.1.0",
defaultConfig: {
name: 'recents',
// Mode of operation:
// memory = show recent photos. requires numDaystoInclude
// album = show picture from album. requires albumId/albumName
// search = search for photos based on a query. requires query
// random = show random photos.
// anniversary = show photos from the same date range across multiple years. requires anniversaryDatesBack, anniversaryDatesForward, anniversaryStartYear, anniversaryEndYear
mode: MODE_MEMORY,
// an Immich API key to be able to access Immich
apiKey: 'provide your API KEY',
// Base Immich URL. /api will be appended to this URL to make API calls.
url: 'provide your base Immich URL',
// The amount of timeout for immich API calls
timeout: 6000,
// Number of days to include images for, including today
numDaysToInclude: 7,
// The ID of the album to display
albumId: null,
// The Name of the album to display
albumName: null,
// When mode is search, we need to query for something
query: null,
// How many images to bring back when searching or random mode (between 1 and 1000)
querySize: 100,
// Number of days to look back before the current date
anniversaryDatesBack: 3,
// Number of days to look forward after the current date
anniversaryDatesForward: 3,
// Starting year for anniversary search
anniversaryStartYear: 2020,
// Ending year for anniversary search
anniversaryEndYear: 2025,
// the speed at which to switch between images, in milliseconds
slideshowSpeed: 15 * 1000,
// how to sort images: name, random, created, modified, taken, none
sortImagesBy: 'none',
// whether to sort in ascending (default) or descending order
sortImagesDescending: false,
// a comma separated list of values to display: name, date, since, geo, exif
imageInfo: ['date', 'since', 'count'],
// the date format to use for imageInfo
dateFormat: DEFAULT_DATE_FORMAT,
// whether to cycle through configs after reaching the last image
cyclicConfigs: false
},
// Default module config.
defaults: {
immichConfigs: [],
activeImmichConfigIndex: 0,
// list of valid file extensions, separated by commas
validImageFileExtensions: 'bmp,jpg,jpeg,gif,png,heic',
// show a panel containing information about the image currently displayed.
showImageInfo: false,
// location of the info div
imageInfoLocation: 'bottomRight', // Other possibilities are: bottomLeft, topLeft, topRight
// remove the file extension from image name
imageInfoNoFileExt: false,
// show a progress bar indicating how long till the next image is displayed.
showProgressBar: false,
// the color of the background when the image does not take up the full screen
backgroundColor: '#000', // can also be rbga(x,y,z,alpha)
// the filter to apply to the background. Useful to give the background a translucent effect
backdropFilter: 'blur(15px)',
// the sizing of the background image
// cover: Resize the background image to cover the entire container, even if it has to stretch the image or cut a little bit off one of the edges
// contain: Resize the background image to make sure the image is fully visible
backgroundSize: 'cover', // cover or contain
// if backgroundSize contain, determine where to zoom the picture. Towards top, center or bottom
backgroundPosition: 'center', // Most useful options: "top" or "center" or "bottom"
// Whether to scroll larger pictures rather than cut them off
backgroundAnimationEnabled: false,
// How long the scrolling animation should take - if this is more than slideshowSpeed, then images do not scroll fully.
// If it is too fast, then the image may appear jittery. For best result, by default we match this to slideshowSpeed.
// For now, it is not documented and will default to match slideshowSpeed.
backgroundAnimationDuration: '1s',
// How many times to loop the scrolling back and forth. If the value is set to anything other than infinite, the
// scrolling will stop at some point since we reuse the same div1.
// For now, it is not documented and is defaulted to infinite.
backgroundAnimationLoopCount: 'infinite',
// transition from one image to the other (may be a bit choppy on slower devices, or if the images are too big)
transitionImages: false,
// transition speed from one image to the other, transitionImages must be true
transitionSpeed: '2s',
// Transitions to use
transitions: [
'opacity',
'slideFromRight',
'slideFromLeft',
'slideFromTop',
'slideFromBottom',
'slideFromTopLeft',
'slideFromTopRight',
'slideFromBottomLeft',
'slideFromBottomRight',
'flipX',
'flipY'
],
transitionTimingFunction: 'cubic-bezier(.17,.67,.35,.96)',
animations: ['slide', 'zoomOut', 'zoomIn'],
showBlurredImageForBlackBars: false,
// Width and height of the module when used in a non-fullscreen position.
// Required for any position other than fullscreen_above / fullscreen_below.
// Accepts any valid CSS value, e.g. '400px', '50vw', '30vh'.
width: null,
height: null
},
// load function
start: function () {
Log.debug(
LOG_PREFIX + 'starting...'
);
// add identifier to the config
this.config.identifier = this.identifier;
// commented out since this was not doing anything
// set no error
// this.errorMessage = null;
Log.debug(LOG_PREFIX + 'current config', this.config);
Log.debug(LOG_PREFIX + 'immichConfigs', this.config.immichConfigs);
// Make sure we have at least one immich config
if (this.config.immichUrl || this.config.apiKey) {
// This is the old config so try and creat a default config using the old values
Log.warn(
LOG_PREFIX + 'You are using the old configuration format which is depricated and will not be supported in the furture. Please update your configuration!'
);
this.showLegacyNotification = true;
// setTimeout( () => {
// this.sendNotification('SHOW_ALERT', {
// type: 'notification',
// title: 'MMM-ImmichSlideShow: Out of date configuration',
// message: 'You are using the old configuration format which is depricated and will not be supported in the furture. Please update your configuration!',
// });
// }, 10000);
this.config.immichConfigs = [
{
name: 'AUTO_GENERATED_LEGACY',
mode: this.config.mode || this.defaultConfig.mode,
apiKey: this.config.apiKey || this.defaultConfig.apiKey,
url: this.config.immichUrl || this.defaultConfig.url,
timeout: this.config.immichTimeout || this.defaultConfig.timeout,
numDaysToInclude: this.config.numDaysToInclude || this.defaultConfig.numDaysToInclude,
albumId: this.config.albumId || this.defaultConfig.albumId,
albumName: this.config.albumName || this.defaultConfig.albumName,
anniversaryDatesBack: this.config.anniversaryDatesBack || this.defaultConfig.anniversaryDatesBack,
anniversaryDatesForward: this.config.anniversaryDatesForward || this.defaultConfig.anniversaryDatesForward,
anniversaryStartYear: this.config.anniversaryStartYear || this.defaultConfig.anniversaryStartYear,
anniversaryEndYear: this.config.anniversaryEndYear || this.defaultConfig.anniversaryEndYear,
slideshowSpeed: this.config.slideshowSpeed || this.defaultConfig.slideshowSpeed,
sortImagesBy: this.config.sortImagesBy || this.defaultConfig.sortImagesBy,
sortImagesDescending: this.config.sortImagesDescending || this.defaultConfig.sortImagesDescending,
imageInfo: this.config.imageInfo || this.defaultConfig.imageInfo,
}
]
} else {
// Make sure the defaultConfig is overridden by the main config
this.defaultConfig = {...this.defaultConfig,...this.config};
// Remove the immichConfigs from default config.
delete this.defaultConfig.immichConfigs
this.config.immichConfigs[0] = {...this.defaultConfig,...this.config.immichConfigs[0]};
}
// Now loop through and make sure that all configs have all properties by copying from the
// first config and overriding with the new config
this.config.immichConfigs.forEach((element,idx) => {
// If the entry does not have a dateFormat specified, set it to default
if (!element.hasOwnProperty('dateFormat')) {
element.dateFormat = DEFAULT_DATE_FORMAT;
}
this.config.immichConfigs[idx] = {...this.config.immichConfigs[0],...element};
const curConfig = this.config.immichConfigs[idx];
//validate immich properties
if (curConfig.mode && curConfig.mode.trim().toLowerCase() === MODE_MEMORY) {
curConfig.mode = MODE_MEMORY
// Make sure we have numDaysToInclude
if (!curConfig.numDaysToInclude || isNaN(curConfig.numDaysToInclude) || curConfig.numDaysToInclude < 1) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': memory mode set, but numDaysToInclude does not have a valid value'
);
curConfig.numDaysToInclude = this.defaultConfig.numDaysToInclude;
} else if (curConfig.numDaysToInclude > 14) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': numDaysToInclude cannot exceet 14 days for memory mode'
);
curConfig.numDaysToInclude = 14;
}
} else if (curConfig.mode && curConfig.mode.trim().toLowerCase() === MODE_ALBUM) {
curConfig.mode = MODE_ALBUM
// Make sure we have album name or album id
if ((!curConfig.albumId || curConfig.albumId.length === 0) && (!curConfig.albumName || curConfig.albumName.length === 0)) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': album mode set, but albumId or albumName do not have a valid value'
);
} else if (curConfig.albumId && curConfig.albumName) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': album mode set, but albumId or albumName do not have a valid value'
);
// This is a double check to make sure we only present one of these properties to
// node_helper
if (curConfig.albumId) {
curConfig.albumName = null;
} else {
curConfig.albumId = null;
}
}
} else if (curConfig.mode && curConfig.mode.trim().toLowerCase() === MODE_SEARCH) {
curConfig.mode = MODE_SEARCH
// Make sure we have album name or album id
if (!curConfig.query || typeof curConfig.query !== 'object') {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': search mode set, but query not provided or set incorrectly'
);
} else if (isNaN(curConfig.querySize) || curConfig.querySize < 1 || curConfig.querySize > 1000) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': search mode set, but querySize must be between 1 and 1000'
);
curConfig.querySize = this.defaultConfig.querySize;
}
} else if (curConfig.mode && curConfig.mode.trim().toLowerCase() === MODE_RANDOM) {
curConfig.mode = MODE_RANDOM
// Validate querySize if provided
if (isNaN(curConfig.querySize) || curConfig.querySize < 1 || curConfig.querySize > 1000) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': random mode set, but querySize must be between 1 and 1000'
);
curConfig.querySize = this.defaultConfig.querySize;
}
} else if (curConfig.mode && curConfig.mode.trim().toLowerCase() === MODE_ANNIVERSARY) {
curConfig.mode = MODE_ANNIVERSARY
// Validate anniversary configuration
if (isNaN(curConfig.anniversaryDatesBack) || curConfig.anniversaryDatesBack < 0) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': anniversary mode set, but anniversaryDatesBack must be a non-negative number'
);
curConfig.anniversaryDatesBack = this.defaultConfig.anniversaryDatesBack;
}
if (isNaN(curConfig.anniversaryDatesForward) || curConfig.anniversaryDatesForward < 0) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': anniversary mode set, but anniversaryDatesForward must be a non-negative number'
);
curConfig.anniversaryDatesForward = this.defaultConfig.anniversaryDatesForward;
}
if (isNaN(curConfig.anniversaryStartYear) || curConfig.anniversaryStartYear < 1900) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': anniversary mode set, but anniversaryStartYear must be a valid year (>= 1900)'
);
curConfig.anniversaryStartYear = this.defaultConfig.anniversaryStartYear;
}
if (isNaN(curConfig.anniversaryEndYear) || curConfig.anniversaryEndYear < curConfig.anniversaryStartYear) {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': anniversary mode set, but anniversaryEndYear must be >= anniversaryStartYear'
);
curConfig.anniversaryEndYear = this.defaultConfig.anniversaryEndYear;
}
} else {
Log.warn(
LOG_PREFIX + 'config ' + idx + ': memory mode not set to valid value, assuming memory mode...'
);
}
// ensure image order is in lower case
this.config.immichConfigs[idx].sortImagesBy = this.config.immichConfigs[idx].sortImagesBy.toLowerCase();
// Make sure to process imageInfo for all entries
if (element.imageInfo) {
this.config.immichConfigs[idx].imageInfo = this.fixImageInfo(element.imageInfo, idx)
}
});
// ensure file extensions are lower case
this.config.validImageFileExtensions = this.config.validImageFileExtensions.toLowerCase();
// Create the activeConfig
if (this.config.activeImmichConfigIndex < 0) {
this.config.activeImmichConfigIndex = 0;
}
this.config.activeImmichConfig = this.config.immichConfigs[this.config.activeImmichConfigIndex < this.config.immichConfigs.length ? this.config.activeImmichConfigIndex : 0];
if (this.data.position.indexOf('fullscreen') !== -1 && (this.config.width || this.config.height)) {
Log.warn(
LOG_PREFIX + 'Display is set to fullscreen and width/height provided. Ignoring with/height...'
);
this.config.width = this.config.height = null;
} else if (this.data.position.indexOf('fullscreen') === -1 && (!this.config.width || !this.config.height)) {
Log.warn(
LOG_PREFIX + 'Display is not fullscreen and width/height not provided. Using defaults...'
);
this.config.width = this.config.width || '480px';
this.config.height = this.config.height || '320px';
}
if (!this.config.transitionImages) {
this.config.transitionSpeed = '0';
}
// Lets make sure the backgroundAnimation duration matches the slideShowSpeed unless it has been
// overridden
if (this.config.backgroundAnimationDuration === '1s') {
this.config.backgroundAnimationDuration =
this.config.activeImmichConfig.slideshowSpeed / 1000 + 's';
}
// Chrome versions < 81 do not support EXIF orientation natively. A CSS transformation
// needs to be applied for the image to display correctly - see http://crbug.com/158753 .
this.browserSupportsExifOrientationNatively = CSS.supports(
'image-orientation: from-image'
);
},
getScripts: function () {
return [
'moment.js'
];
},
getStyles: function () {
// the css contains the make grayscale code
return ['immichSlideShow.css'];
},
// generic notification handler
notificationReceived: function (notification, payload, sender) {
Log.debug(LOG_PREFIX + 'notificationReceived', notification, ' || Payload: ', (payload ? JSON.stringify(payload) : '<undefined>'), ' || Sender: ', sender);
if (notification === 'DOM_OBJECTS_CREATED') {
Log.debug(LOG_PREFIX + 'Sending register API notification for ' + this.name);
const actions = {
showNext: {
method: 'GET',
notification: "IMMICHSLIDESHOW_NEXT",
prettyName: 'Show next picture'
},
showPrevisous: {
method: 'GET',
notification: "IMMICHSLIDESHOW_PREVIOUS",
prettyName: 'Show previous picture'
},
pause: {
method: 'GET',
notification: "IMMICHSLIDESHOW_PAUSE",
prettyName: 'Pause slide show'
},
resume: {
method: 'GET',
notification: "IMMICHSLIDESHOW_RESUME",
prettyName: 'Resume slide show'
}
};
this.config.immichConfigs.forEach((config, idx) => {
actions[`setConfigIndex${idx}`] = {
method: 'POST',
notification: "IMMICHSLIDESHOW_SET_ACTIVE_CONFIG",
payload: {data: idx},
prettyName: `Make config ${idx} active`
}
});
// Add our own definition
this.sendNotification('REGISTER_API', {
module: this.name,
path: this.name.substring(4).toLowerCase(),
actions: actions
});
// } else if (notification === 'IMMICHSLIDESHOW_UPDATE_IMAGE_LIST') {
// this.suspend();
// this.updateImageList();
// this.updateImage();
// // Restart timer only if timer was already running
// this.resume();
// } else if (notification === 'IMMICHSLIDESHOW_IMAGE_UPDATE') {
// Log.debug(LOG_PREFIX + 'Changing Background');
// this.suspend();
// // this.updateImage();
// this.resume();
} else if (notification === 'IMMICHSLIDESHOW_NEXT') {
this.suspend();
// Change to next image
this.updateImage();
// Restart timer only if timer was already running
this.resume();
} else if (notification === 'IMMICHSLIDESHOW_PREVIOUS') {
this.suspend();
// Change to previous image
this.updateImage(/* skipToPrevious= */ true);
// Restart timer only if timer was already running
this.resume();
} else if (notification === 'IMMICHSLIDESHOW_RESUME') {
this.resume();
} else if (notification === 'IMMICHSLIDESHOW_PAUSE') {
this.suspend();
} else if (notification === 'IMMICHSLIDESHOW_SET_ACTIVE_CONFIG') {
// Update config in backend
this.setActiveConfig(payload.data);
} else if (notification === 'ALL_MODULES_STARTED') {
if (this.showLegacyNotification) {
this.sendNotification('SHOW_ALERT', {
type: 'notification',
title: 'MMM-ImmichSlideShow',
message: 'You are using the old configuration format which is depricated and will not be supported in the furture. Please update your module configuration!',
});
}
} else {
Log.debug(LOG_PREFIX + 'received an unexpected system notification: ' + notification);
}
},
// the socket handler
socketNotificationReceived: function (notification, payload) {
Log.debug(LOG_PREFIX + 'socketNotificationReceived', notification, ' || Payload: ', payload ? JSON.stringify(payload) : '<null>');
// check this is for this module based on the id
if (!!payload.identifier && payload.identifier === this.identifier) {
// check this is for this module based on the woeid
if (notification === 'IMMICHSLIDESHOW_READY') {
this.suspend();
this.resume();
} else if (notification === 'IMMICHSLIDESHOW_FILELIST') {
// bubble up filelist notifications
this.imageList = payload;
// Log.debug (LOG_PREFIX + " >>>>>>>>>>>>>>> IMAGE LIST", JSON.stringify(payload));
} else if (notification === 'IMMICHSLIDESHOW_DISPLAY_IMAGE') {
Log.debug(LOG_PREFIX + 'Displaying current image', payload.path);
// Create an interval timer that if not called will attempt to establish configuration again.
// Apparently, the socket will keep retrying until it connects, so we only need to reattempt once.
if (!!this.resyncTimeout) {
console.debug('this.resyncTimeout', this.resyncTimeout);
clearTimeout(this.resyncTimeout);
}
const me = this;
this.resyncTimeout = setTimeout(() => {
console.log('Re-registering to make sure images change...')
me.updateImageList();
}, me.config.activeImmichConfig.slideshowSpeed+me.config.activeImmichConfig.timeout);
this.displayImage(payload);
} else if (notification === 'IMMICHSLIDESHOW_REGISTER_CONFIG') {
// Update config in backend
this.updateImageList();
} else if (notification === 'IMMICHSLIDESHOW_CONFIG_CHANGED') {
// Config was changed due to cyclic configs
Log.debug(LOG_PREFIX + 'Config changed by cycling to index: ' + payload.configIndex);
this.config.activeImmichConfigIndex = payload.configIndex;
this.config.activeImmichConfig = this.config.immichConfigs[payload.configIndex];
} else {
Log.warn(LOG_PREFIX + 'received an unexpected module notification: ' + notification);
}
}
},
createDiv: function () {
let div = document.createElement('div');
div.style.backgroundSize = this.config.backgroundSize;
div.style.backgroundPosition = this.config.backgroundPosition;
div.className = 'image';
return div;
},
createImageInfoDiv: function (wrapper) {
const div = document.createElement('div');
div.className = 'info ' + this.config.imageInfoLocation;
wrapper.appendChild(div);
return div;
},
createProgressbarDiv: function (wrapper, slideshowSpeed) {
const div = document.createElement('div');
div.className = 'progress';
const inner = document.createElement('div');
inner.className = 'progress-inner';
inner.style.display = 'none';
inner.style.animation = `move ${slideshowSpeed}ms linear`;
div.appendChild(inner);
wrapper.appendChild(div);
},
displayImage: function (imageinfo) {
const imageInfo = imageinfo;
const image = new Image();
image.onload = () => {
// check if there are more than 2 elements and remove the first one
if (this.imagesDiv.childNodes.length > 1) {
this.imagesDiv.removeChild(this.imagesDiv.childNodes[0]);
}
if (this.imagesDiv.childNodes.length > 0 && this.data.position.indexOf('fullscreen') === -1) {
this.imagesDiv.removeChild(this.imagesDiv.childNodes[0]);
//this.imagesDiv.childNodes[0].style.opacity = '0';
}
const transitionDiv = document.createElement('div');
transitionDiv.className = 'transition';
// transitionDiv.innerHTML = " <span style=\"background-color: #f00, color: #ff0\">Hello there</span> ";
// Create a background color around the image is not see through
if (this.config.showBlurredImageForBlackBars) {
transitionDiv.style.backdropFilter = this.config.backdropFilter || 'blur(10px)';
}
if (this.config.backgroundSize == 'contain' && this.config.showBlurredImageForBlackBars) {
this.imagesDiv.style.backgroundImage = `url("${image.src}")`;
} else if (this.config.backgroundSize == 'contain' ) {
transitionDiv.style.backgroundColor = this.config.backgroundColor || 'rgba(0,0,0,0.5)';
} else {
this.imagesDiv.style.backgroundColor = this.config.backgroundColor || 'rgba(0,0,0,0.5)';
}
if (this.config.transitionImages && this.config.transitions.length > 0) {
let randomNumber = Math.floor(
Math.random() * this.config.transitions.length
);
transitionDiv.style.animationDuration = this.config.transitionSpeed;
transitionDiv.style.transition = `opacity ${this.config.transitionSpeed} ease-in-out`;
transitionDiv.style.animationName = this.config.transitions[
randomNumber
];
transitionDiv.style.animationTimingFunction = this.config.transitionTimingFunction;
}
const imageDiv = this.createDiv();
imageDiv.style.backgroundImage = `url("${image.src}")`;
if (this.config.showProgressBar) {
// Restart css animation
const oldDiv = document.getElementsByClassName('progress-inner')[0];
const newDiv = oldDiv.cloneNode(true);
// Make sure the new clone's style is set according to our new slideshow speed
newDiv.style.animation = `move ${this.config.activeImmichConfig.slideshowSpeed}ms linear`;
oldDiv.parentNode.replaceChild(newDiv, oldDiv);
newDiv.style.display = '';
}
// Check to see if we need to animate the background
if (
this.config.backgroundAnimationEnabled &&
this.config.animations.length
) {
randomNumber = Math.floor(
Math.random() * this.config.animations.length
);
const animation = this.config.animations[randomNumber];
imageDiv.style.animationDuration = this.config.backgroundAnimationDuration;
imageDiv.style.animationDelay = this.config.transitionSpeed;
if (animation === 'slide') {
imageDiv.style.backgroundPosition = '';
imageDiv.style.animationIterationCount = this.config.backgroundAnimationLoopCount;
imageDiv.style.backgroundSize = 'cover';
// check to see if the width of the picture is larger or the height
let width = image.width;
let height = image.height;
let adjustedWidth = (width * window.innerHeight) / height;
let adjustedHeight = (height * window.innerWidth) / width;
if (
adjustedWidth / window.innerWidth >
adjustedHeight / window.innerHeight
) {
// Scrolling horizontally...
if (Math.floor(Math.random() * 2)) {
imageDiv.className += ' slideH';
} else {
imageDiv.className += ' slideHInv';
}
} else {
// Scrolling vertically...
if (Math.floor(Math.random() * 2)) {
imageDiv.className += ' slideV';
} else {
imageDiv.className += ' slideVInv';
}
}
} else {
imageDiv.className += ` ${animation}`;
}
}
if (this.config.showImageInfo) {
let dateTime = 'N/A';
if (imageInfo.exifInfo) {
dateTime = imageInfo.exifInfo.dateTimeOriginal;
// attempt to parse the date if possible
if (dateTime !== null) {
try {
dateTime = moment(dateTime);
} catch (e) {
Log.debug(
LOG_PREFIX + 'Failed to parse dateTime: ' +
dateTime
);
dateTime = 'Invalid date';
}
}
}
// Update image info
this.updateImageInfo(imageInfo, dateTime);
}
if (!this.browserSupportsExifOrientationNatively) {
const exifOrientation = imageInfo.exifInfo.orientation;
imageDiv.style.transform = this.getImageTransformCss(exifOrientation);
}
transitionDiv.appendChild(imageDiv);
this.imagesDiv.appendChild(transitionDiv);
};
image.src = imageInfo.data;
this.sendNotification('IMMICHSLIDESHOW_IMAGE_UPDATED', {
url: imageInfo.path
});
},
updateImage: function (backToPreviousImage = false) {
Log.debug(LOG_PREFIX + 'updateImage called... backtoPrevious?', backToPreviousImage);
if (backToPreviousImage) {
this.sendSocketNotification('IMMICHSLIDESHOW_PREV_IMAGE');
} else {
this.sendSocketNotification('IMMICHSLIDESHOW_NEXT_IMAGE');
}
},
getImageTransformCss: function (exifOrientation) {
switch (exifOrientation) {
case 2:
return 'scaleX(-1)';
case 3:
return 'scaleX(-1) scaleY(-1)';
case 4:
return 'scaleY(-1)';
case 5:
return 'scaleX(-1) rotate(90deg)';
case 6:
return 'rotate(90deg)';
case 7:
return 'scaleX(-1) rotate(-90deg)';
case 8:
return 'rotate(-90deg)';
case 1: // Falls through.
default:
return 'rotate(0deg)';
}
},
/// Function to format EXIF data
formatExposureTime: function(exposureTime) {
if (!exposureTime) return null;
let numericValue;
if (typeof exposureTime === 'string') {
// Case "1/xxx"
if (exposureTime.includes('/')) {
const parts = exposureTime.replace('s', '').split('/');
if (parts.length === 2) {
const numerator = parseFloat(parts[0]);
const denominator = parseFloat(parts[1]);
if (numerator === 1 && !isNaN(denominator) && denominator !== 0) {
numericValue = 1 / denominator;
} else {
return null; // Invalid format
}
} else {
return null;
}
} else {
numericValue = parseFloat(exposureTime);
}
} else {
numericValue = parseFloat(exposureTime);
}
if (isNaN(numericValue)) return null;
if (numericValue >= 1) {
return numericValue % 1 === 0 ? `${numericValue}s` : `${numericValue.toFixed(1)}s`;
} else {
const denominator = Math.round(1 / numericValue);
if (denominator <= 125) {
// From 1/125s to 1s → round 5
const rounded = Math.round(denominator / 5) * 5;
return `1/${rounded}s`;
} else if (denominator <= 500) {
// Up to 1/500s → round 50
const rounded = Math.round(denominator / 50) * 50;
return `1/${rounded}s`;
} else {
// Faster times → round 100
const rounded = Math.round(denominator / 100) * 100;
return `1/${rounded}s`;
}
}
},
formatFocalLength: function(focalLength) {
if (!focalLength) return null;
return `${Math.round(parseFloat(focalLength))}mm`;
},
formatAperture: function(fNumber) {
if (!fNumber) return null;
return `f/${parseFloat(fNumber).toFixed(1)}`;
},
formatISO: function(iso) {
if (!iso) return null;
return `ISO ${iso}`;
},
updateImageInfo: function (imageinfo, imageDate) {
let imageProps = [];
const config = this.config.activeImmichConfig;
config.imageInfo.forEach((prop, idx) => {
switch (prop) {
case 'date': // show date image was taken
if (imageDate && imageDate !== 'Invalid date') {
imageProps.push(imageDate.format(config.dateFormat));
}
break;
case 'since': // show how long since the image was taken
if (imageDate && imageDate !== 'Invalid date') {
imageProps.push(imageDate.fromNow());
}
break;
case 'name': // default is name
// Only display last path component as image name if recurseSubDirectories is not set.
let imageName = imageinfo.path.split('/').pop();
// Remove file extension from image name.
if (this.config.imageInfoNoFileExt) {
imageName = imageName.substring(0, imageName.lastIndexOf('.'));
}
imageProps.push(imageName);
break;
case 'geo': // show image location
let geoLocation = '';
if (imageinfo.exifInfo) {
geoLocation = imageinfo.exifInfo.city ?? '';
geoLocation += imageinfo.exifInfo.state ? `, ${imageinfo.exifInfo.state}` : '';
geoLocation += imageinfo.exifInfo.country ? `, ${imageinfo.exifInfo.country}` : '';
// In case some values are null and our geo starts with comma, then strip it.
if (geoLocation.startsWith(',')) {
geoLocation = geoLocation.substring(2);
}
}
// If we end up with a string that has some length, then add it to image info.
if (geoLocation.length > 0) {
imageProps.push(geoLocation);
}
break;
// Show EXIF data
case 'exif': // show EXIF technical data
if (imageinfo.exifInfo) {
let exifData = [];
// Aperture (f-stop)
const aperture = this.formatAperture(imageinfo.exifInfo.fNumber);
if (aperture) exifData.push(aperture);
// Exposition time
const exposureTime = this.formatExposureTime(imageinfo.exifInfo.exposureTime);
if (exposureTime) exifData.push(exposureTime);
// Focal lenght
const focalLength = this.formatFocalLength(imageinfo.exifInfo.focalLength);
if (focalLength) exifData.push(focalLength);
// ISO
const iso = this.formatISO(imageinfo.exifInfo.iso);
if (iso) exifData.push(iso);
if (exifData.length > 0) {
imageProps.push(exifData.join(' • '));
}
}
break;
case 'people':
case 'people_skip': // show people in image
// Only display last path component as image name if recurseSubDirectories is not set.
if (Array.isArray(imageinfo.people)) {
let peopleName = '';
imageinfo.people.forEach((people, idx) => {
const personName = people.name || '?';
// Person name must be greater than 1 since at min it would be set to ?
// Only add people name if it is set or we are not skipping
if ((prop=='people' || (prop=='people_skip' && personName.length > 1))) {
// Add a comma between the people's names if not the first
if (peopleName.length > 0 && idx > 0 ) {
peopleName += ', ';
}
peopleName += personName;
}
if (people.birthDate && this.config.activeImmichConfig.imageInfo.includes('age')) {
peopleName += `(${this.getAgeFromDate(people.birthDate, imageDate)})`
}
})
// Remove file extension from image name.
if (peopleName.length > 0) {
imageProps.push(peopleName);
}
}
break;
case 'age': // show people's age in images
break;
case 'desc': // show description of images
if (imageinfo.exifInfo && imageinfo.exifInfo.description) {
Log.debug(
LOG_PREFIX + 'Description: ' + imageinfo.exifInfo.description);
imageProps.push(imageinfo.exifInfo.description);
}
break;
case 'count': // show image count
case 'album':
break;
default:
Log.warn(
LOG_PREFIX + prop +
' is not a valid value for imageInfo. Please check your configuration'
);
}
});
// Log.debug('config.imageInfo[count]', config.imageInfo.includes('count'));
// Log.dir('config.imageInfo', config.imageInfo);
let headerString = '';
if (config.imageInfo.includes('count')) {
headerString = `${imageinfo.index} of ${imageinfo.total}`;
}
if (config.imageInfo.includes('album')) {
headerString = headerString.length == 0 ? imageinfo.albumName : headerString + '<br>' + imageinfo.albumName;
}
let innerHTML = headerString.length > 0 ? `<header class="infoDivHeader">${headerString}</header>`: '';
imageProps.forEach((val, idx) => {
innerHTML += val + '<br/>';
});
this.imageInfoDiv.innerHTML = innerHTML;
imageProps = null;
},
suspend: function () {
Log.debug(LOG_PREFIX + 'Suspend called...');
// Hide the progress while paused
if (this.config.showProgressBar) {
const oldDiv = document.getElementsByClassName('progress-inner')[0];
if (oldDiv) {
oldDiv.style.display = 'none';
}
}
this.sendSocketNotification(
'IMMICHSLIDESHOW_SUSPEND'
);
},
resume: function () {
Log.debug(LOG_PREFIX + 'Resume called...');
// this.suspend();
this.sendSocketNotification(
'IMMICHSLIDESHOW_RESUME'
);
},
updateImageList: function () {
Log.debug(LOG_PREFIX + 'updateImageList called...');
// this.suspend();
// Log.debug(LOG_PREFIX + 'Getting Images');
// ask helper function to get the image list
this.sendSocketNotification(
'IMMICHSLIDESHOW_REGISTER_CONFIG',
this.config
);
},
setActiveConfig: function (configIndex) {
Log.debug(LOG_PREFIX + 'setActiveConfig called...', configIndex, !isNaN(configIndex), configIndex < this.config.immichConfigs.length);
// Validate that the payload is good. The id has already been validated
if (!isNaN(configIndex) && configIndex > -1 && configIndex < this.config.immichConfigs.length) {
this.config.activeImmichConfig = this.config.immichConfigs[configIndex];
this.config.activeImmichConfigIndex = configIndex;
Log.debug(LOG_PREFIX + 'new active config', this.config.activeImmichConfig);
// ask helper function to get the image list
this.sendSocketNotification(
'IMMICHSLIDESHOW_REGISTER_CONFIG',
this.config
);
} else {
Log.debug(LOG_PREFIX + 'bad parameter passed to setActiveConfig:', configIndex);
}
},
getAgeFromDate: function (dateString, imageDate) {
var today = imageDate || moment();
var birthDate = moment(dateString);
var duration = moment.duration(today.diff(birthDate));
var y = duration.asYears();
var m = duration.asMonths();
var d = duration.asDays();
if (y >= 1) {
age = Math.floor(y);
} else if (m >= 1) {
age = `${Math.floor(m)}m`
} else {
age = `${Math.floor(d)}d`
}
return age;
},
/**
* This funciton checks the value of imageInfo and process it to convert it
* to an array
* @param {array/string} imageInfo
* @returns
*/
fixImageInfo: function(imageInfo, index) {
//validate imageinfo property. This will make sure we have at least 1 valid value
// AGGIORNATA: Aggiunto 'exif' alla lista dei valori validi
const imageInfoValues = '\\bname\\b|\\bdate\\b|\\bsince\\b|\\bgeo\\b|\\bpeople\\b|\\bpeople_skip\\b|\\bage\\b|\\bdesc\\b|\\bcount\\b|\\bexif\\b|';
const imageInfoRegex = new RegExp(imageInfoValues,'gi');
// Set the log prefix
const prefix = LOG_PREFIX + `config[${index}]: `;
let setToDefault = false;
let newImageInfo = [];
if (
Array.isArray(imageInfo)
) {
for (const [i, infoItem] of Object.entries(imageInfo)) {
console.debug(prefix + 'Checking imageInfo: ', i, infoItem);
// Skip any entries that do not have a matching value
if (imageInfoValues.substring(infoItem.trim().toLowerCase())) {
// Make sure to trim the entries and make them lowercase
newImageInfo.push(infoItem.trim().toLowerCase());
} else {
console.warn(prefix + `invalid image info item '${infoItem}'`);
}
}
// If nothing matched, then use default
if (newImageInfo.length === 0) {
setToDefault = true;
}
} else if (!imageInfoRegex.test(imageInfo)) {
Log.warn(
prefix + 'showImageInfo is set, but imageInfo does not have a valid value. Using date as default!'
);
setToDefault = true;
} else {
// convert to lower case and replace any spaces with , to make sure we get an array back
// even if the user provided space separated values
newImageInfo = imageInfo
.toLowerCase()
.replace(/\s/g, ',')
.split(',');
// now filter the array to only those that have values
newImageInfo = newImageInfo.filter((n) => n);
}
// The imageInfo params had invalid values in them
if (setToDefault) {
// Use name as the default
newImageInfo = this.defaultConfig.imageInfo;
} else {
if (newImageInfo.includes('people') && newImageInfo.includes('people_skip')) {
Log.warn(
prefix + 'imageInfo should not include both people and people_skip. Using people!'