-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathMMM-ImmichSlideShow.js
executable file
·851 lines (782 loc) · 32.2 KB
/
MMM-ImmichSlideShow.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
/* 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'; // TODO
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
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 (between 1 and 1000)
querySize: 100,
// 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
imageInfo: ['date', 'since'],
},
// 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,
// The compression level of the resulting jpeg
imageCompression: 0.7,
// 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
},
// 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,
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 {
this.config.immichConfigs[0] = {...this.defaultConfig,...this.config.immichConfigs[0]};
}
// Validate that we have enough for the first config. Since this will be used as the base
// for all other configs, then validating this alone should be good enough
const firstConfig = this.config.immichConfigs[0];
//validate immich properties
if (firstConfig.mode && firstConfig.mode.trim().toLowerCase() === MODE_MEMORY) {
firstConfig.mode = MODE_MEMORY
// Make sure we have numDaysToInclude
if (!firstConfig.numDaysToInclude || isNaN(firstConfig.numDaysToInclude)) {
Log.warn(
LOG_PREFIX + 'memory mode set, but numDaysToInclude does not have a valid value'
);
}
} else if (firstConfig.mode && firstConfig.mode.trim().toLowerCase() === MODE_ALBUM) {
firstConfig.mode = MODE_ALBUM
// Make sure we have album name or album id
if ((!firstConfig.albumId || firstConfig.albumId.length === 0) && (!firstConfig.albumName || firstConfig.albumName.length === 0)) {
Log.warn(
LOG_PREFIX + 'album mode set, but albumId or albumName do not have a valid value'
);
} else if (firstConfig.albumId && firstConfig.albumName) {
Log.warn(
LOG_PREFIX + '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 (firstConfig.albumId) {
firstConfig.albumName = null;
} else {
firstConfig.albumId = null;
}
}
} else {
Log.warn(
LOG_PREFIX + 'memory mode not set to valid value, assuming memory mode...'
);
}
// 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) => {
this.config.immichConfigs[idx] = {...this.config.immichConfigs[0],...element};
// 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.config.transitionImages) {
this.config.transitionSpeed = '0';
}
if (!this.config.imageCompression) {
this.config.imageCompression = 0.7;
} else {
try {
const compressionVal = parseFloat(this.config.imageCompression);
if (compressionVal < 0 || compressionVal > 1) {
Log.warn(
LOG_PREFIX + 'imageCompression should be between 0 and 1! Defaulting to 0.7'
);
this.config.imageCompression = 0.7;
}
} catch (e) {
Log.warn(
LOG_PREFIX + 'imageCompression should be a decimal value between 0 and 1! Defaulting to 0.7'
);
this.config.imageCompression = 0.7;
}
}
// 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'
);
},
/**
* 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
const imageInfoValues = '\\bname\\b|\\bdate\\b|\\bsince\\b|\\bgeo\\b|\\bpeople\\b|\\bpeople_skip\\b|\\bage\\b|\\bdesc\\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 = ['date'];
} else {
if (newImageInfo.includes('people') && newImageInfo.includes('people_skip')) {
Log.warn(
prefix + 'imageInfo should not include both people and people_skip. Using people!'
);
// Remove people_skip since people is already included
newImageInfo = newImageInfo.filter((n) => n !== 'people_skip');
}
if (newImageInfo.includes('age') && !(newImageInfo.includes('people') || newImageInfo.includes('people_skip'))) {
Log.warn(
prefix + 'imageInfo includes age but not people. Removing age from imageInfo!'
);
// Remove age since people is not included
newImageInfo = newImageInfo.filter((n) => n !== 'age');
}
}
return newImageInfo;
},
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.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 {
Log.warn(LOG_PREFIX + 'received an unexpected module notification: ' + notification);
}
}
},
// Override dom generator.
getDom: function () {
let wrapper = document.createElement('div');
this.imagesDiv = document.createElement('div');
this.imagesDiv.className = 'images';
if (this.config.backgroundSize == 'contain' && this.config.showBlurredImageForBlackBars) {
this.imagesDiv.style.backgroundSize = 'cover';
this.imagesDiv.style.backgroundPosition = 'center';
}
wrapper.appendChild(this.imagesDiv);
if (this.config.showImageInfo) {
this.imageInfoDiv = this.createImageInfoDiv(wrapper);
}
if (this.config.showProgressBar) {
this.createProgressbarDiv(wrapper, this.config.activeImmichConfig.slideshowSpeed);
}
if (this.config.activeImmichConfig.apiKey.length == 0) {
Log.error(
LOG_PREFIX + 'Missing required parameter apiKey.'
);
} else {
this.updateImageList();
}
return wrapper;
},
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.imagesDiv.childNodes[0].style.opacity = '0';
}
const transitionDiv = document.createElement('div');
transitionDiv.className = 'transition';
// 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 {
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 +
' to format YYYY:MM:DD HH:mm:ss'
);
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)';
}
},
updateImageInfo: function (imageinfo, imageDate) {
let imageProps = [];
this.config.activeImmichConfig.imageInfo.forEach((prop, idx) => {
switch (prop) {
case 'date': // show date image was taken
if (imageDate && imageDate !== 'Invalid date') {
imageProps.push(imageDate.format('dddd MMMM D, YYYY HH:mm'));
}
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;
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;
default:
Log.warn(
LOG_PREFIX + prop +
' is not a valid value for imageInfo. Please check your configuration'
);
}
});
let innerHTML = `<header class="infoDivHeader">${imageinfo.index} of ${imageinfo.total}</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;
}
});