Browse Source

Fix #1 display media 15 per 15

isundil 1 năm trước cách đây
mục cha
commit
16c9a631b0

+ 1 - 1
router/api.js

@@ -57,7 +57,7 @@ module.exports = { register: app => {
             data: await MediaService.fetchMediasWithAccess(
                 app,
                 isNaN(fromDate) ? 0 : fromDate,
-                isNaN(count) ? 25 : Math.min(75, count),
+                isNaN(count) ? 25 : Math.min(150, count),
                 req.sessionObj?.accessList),
             first: first,
             last: last

+ 2 - 2
router/bootstrap.js

@@ -1,7 +1,7 @@
 
 module.exports = { register: app => {
-    app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap.min.js", './node_modules/bootstrap/dist/js/bootstrap.bundle.min.js');
-    app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap.min.js.map", './node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map');
+    app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap.bundle.min.js", './node_modules/bootstrap/dist/js/bootstrap.bundle.min.js');
+    app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap.bundle.min.js.map", './node_modules/bootstrap/dist/js/bootstrap.bundle.min.js.map');
     app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap.min.css", './node_modules/bootstrap/dist/css/bootstrap.min.css');
     app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap.min.css.map", './node_modules/bootstrap/dist/css/bootstrap.min.css.map');
     app.routerUtils.staticGet(app, "/public/bootstrap/bootstrap-icons.min.css", './node_modules/bootstrap-icons/font/bootstrap-icons.min.css');

+ 4 - 1
router/mdi.js

@@ -10,8 +10,11 @@ module.exports = { register: app => {
     app.routerUtils.staticGet(app, "/public/fonts/materialdesignicons-webfont.woff", './node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff');
     app.routerUtils.staticGet(app, "/public/fonts/materialdesignicons-webfont.woff2", './node_modules/@mdi/font/fonts/materialdesignicons-webfont.woff2');
     app.routerUtils.staticGet(app, "/public/js/BsMultiSelect.min.js", './node_modules/@dashboardcode/bsmultiselect/dist/js/BsMultiSelect.min.js');
-    app.routerUtils.staticGet(app, "/public/css/BsMultiSelect.min.js", './node_modules/@dashboardcode/bsmultiselect/dist/css/BsMultiSelect.min.css');
+    app.routerUtils.staticGet(app, "/public/js/BsMultiSelect.min.js.map", './node_modules/@dashboardcode/bsmultiselect/dist/js/BsMultiSelect.min.js.map');
+    app.routerUtils.staticGet(app, "/public/css/BsMultiSelect.min.css", './node_modules/@dashboardcode/bsmultiselect/dist/css/BsMultiSelect.min.css');
+    app.routerUtils.staticGet(app, "/public/css/BsMultiSelect.min.css.map", './node_modules/@dashboardcode/bsmultiselect/dist/css/BsMultiSelect.min.css.map');
     app.routerUtils.staticGet(app, "/public/js/popper.min.js", './node_modules/@popperjs/core/dist/umd/popper.min.js');
+    app.routerUtils.staticGet(app, "/public/js/popper.min.js.map", './node_modules/@popperjs/core/dist/umd/popper.min.js.map');
     app.routerUtils.staticGet(app, "/public/js/tasks.js", './static/public/js/tasks.js');
     app.routerUtils.staticGet(app, "/favicon.ico", './static/public/img/logo.svg');
 }};

+ 15 - 3
static/public/js/common.js

@@ -58,10 +58,22 @@ $(() => {
         return true;
     });
 
+    onScrollBottom = (function () {
+        this.lastCall= null;
+        this.fnc = () => {
+            if (this.lastCall !== null)
+                return;
+            this.lastCall = setTimeout(() => {
+                this.lastCall = null;
+            }, 1000);
+            window.displayMoreMedia();
+        };
+        return this;
+    })();
+
     window.addEventListener("scroll", evt => {
-        if (window.scrollY+window.innerHeight >= document.body.clientHeight) {
-            ReadMediaList();
-        }
+        if (window.scrollY+window.innerHeight >= document.body.clientHeight)
+            onScrollBottom.fnc();
     });
 
     RebuildAccess();

+ 26 - 224
static/public/js/uiMedia.js

@@ -129,6 +129,11 @@ $(() => {
         return result;
     }
 
+    let lastItemDisplayed = null;
+    let displayedItemCount = 0;
+    const displayItemBatchCount = 15;
+    let targetDisplayedItems = displayItemBatchCount;
+
     function redraw(container, media) {
         buildThumbnail(media);
         if (!media.ui)
@@ -143,6 +148,9 @@ $(() => {
             container.dataset.lastItemMonth = media.date.getUTCMonth();
         }
         container.appendChild(media.ui.root);
+
+        lastItemDisplayed = media;
+        displayedItemCount++;
     }
 
     MediaStorage.Instance.addEventListener("rebuildMedia", (evt) => {
@@ -150,239 +158,33 @@ $(() => {
         newContainer.textContent = '';
         newContainer.dataset.lastItemYear = null;
         newContainer.dataset.lastItemMonth = null;
+        lastItemDisplayed = null;
+        displayedItemCount = 0;
+        targetDisplayedItems = displayItemBatchCount;
         for (let i of MediaStorage.Instance.medias)
-            if (window.FilterManager.match(i))
+            if (window.FilterManager.match(i)) {
                 redraw(newContainer, i);
+                if (displayedItemCount >= targetDisplayedItems)
+                    break;
+            }
     });
 
     MediaStorage.Instance.addEventListener("newMedia", (evt) => {
-        let container = document.getElementById('pch-mediaList');
-        if (window.FilterManager.match(evt.detail))
+        if (displayedItemCount < targetDisplayedItems && window.FilterManager.match(evt.detail)) {
+            let container = document.getElementById('pch-mediaList');
             redraw(container, evt.detail);
-    });
-
-    function serializeFileSize(size) {
-        let units = [ 'o', 'Ko', 'Mo', 'Go', 'To' ];
-        let idx = 0;
-        while (size >= 800 && idx < units.length) {
-            ++idx;
-            size /= 1024;
-        }
-        size = Math.floor(size * 100) / 100;
-        return `${size} ${units[idx]}`;
-    }
-
-    function displayMeta(key, value, isRo) {
-        let li = document.createElement("li");
-        let type = value?.type || null;
-        let val = (value?.value !== undefined ? value.value : value);
-        if (!val && val !== '')
-            return null;
-        if (type === 'date')
-            val = val.toLocaleString();
-        if (["dimension", "height", "width"].indexOf(key) >= 0)
-            val += ' px';
-        if (["fileSize"].indexOf(key) >= 0)
-            val = serializeFileSize(val);
-        if (key == 'fNumber')
-            val = `f/ ${val}`;
-        let keyTranslate = {
-            fileSize: "File Size",
-            photochamberImport: "Photochamber Imported",
-            lensModel: "Lens Model",
-            exposureTimeStr: "Exposure Time"
-        };
-        let keySpan = document.createElement('span');
-        let valSpan = document.createElement('div');
-        li.classList.add("row");
-        keySpan.className = "metaKey col-xl-12 col-4";
-        valSpan.className = "metaVal col-xl-12 col-8";
-        let inputGroup = document.createElement('form');
-        valSpan.appendChild(inputGroup);
-        inputGroup.classList.add('input-group');
-        keySpan.innerText = (keyTranslate[key] || (key[0].toUpperCase()+key.substr(1))) + ':';
-        let valInput = document.createElement("input");
-        valInput.classList.add("form-control");
-        valInput.value = val;
-        valInput.disabled = isRo;
-        valInput.addEventListener('keyup', evt => evt.stopPropagation());
-        valInput.addEventListener('keydown', evt => evt.stopPropagation());
-        inputGroup.appendChild(valInput);
-        if (!isRo) {
-            let bt = document.createElement('button');
-            bt.className = 'btn btn-outline-secondary';
-            bt.type = 'button';
-            bt.innerHTML = '<i class="bi bi-pen"></i>';
-            inputGroup.appendChild(bt);
-            bt.addEventListener('click', () => MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed.md5sum, key, valInput.value));
-            inputGroup.addEventListener('submit', evt => {
-                evt.preventDefault();
-                MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed.md5sum, key, valInput.value);
-            });
-        }
-        li.appendChild(keySpan);
-        li.appendChild(valSpan);
-        return li;
-    }
-
-    function displayMetas(metaData, isRo) {
-        let metaList = document.createElement("ul");
-
-        metaData.libraryPath = null;
-
-        metaData.tags = metaData.fixedTags = null;
-
-        if (metaData.exposureTime && metaData.exposureTimeStr)
-            metaData.exposureTime = null;
-
-        if (metaData.date && metaData.dateTime)
-            metaData.dateTime = null;
-
-        if (metaData.height && metaData.width) {
-            metaData.dimension = { type: "string", value: `${metaData.width.value} x ${metaData.height.value}` }
-            metaData.height = metaData.width = null;
-        }
-
-        for (let i of [ "date", "dimension", "height", "width", "fileSize" ]) {
-            let metaItem = displayMeta(i, metaData[i], true);
-            metaItem && metaList.appendChild(metaItem);
-            metaData[i] = null;
         }
+    });
 
-        for (let i of Object.keys(metaData).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
-            let metaItem = displayMeta(i, metaData[i], isRo || !MediaStorage.Instance.allMetaTypes[i]?.canWrite);
-            metaItem && metaList.appendChild(metaItem);
+    window.displayMoreMedia = () => {
+        targetDisplayedItems += displayItemBatchCount;
+        let container = document.getElementById('pch-mediaList');
+        for (let index = lastItemDisplayed ? (MediaStorage.Instance.getMediaIndex(lastItemDisplayed) +1) : 0;
+            displayedItemCount < targetDisplayedItems; ++index) {
+            let media = MediaStorage.Instance.medias[index];
+            if (window.FilterManager.match(media))
+                redraw(container, media);
         }
-
-        if (!isRo)
-            for (let i of ['geoCountry']) {
-                if (!metaData[i]) {
-                    let metaItem = displayMeta(i, "", false);
-                    metaItem && metaList.appendChild(metaItem);
-                }
-            }
-        return metaList;
     }
-
-    function displayTags(fixedTagList, tagList, writeAccess) {
-        let tagListUi = document.createElement("ul");
-        tagListUi.className = "taglist";
-        let createTagUiItem = (i, roTag) => {
-            let uiItem = document.createElement("li");
-            uiItem.className = "badge text-bg-light";
-            let textItem = document.createElement("span");
-            textItem.textContent = i;
-            uiItem.appendChild(textItem);
-            if (!roTag) {
-                let btItem = document.createElement("a");
-                btItem.className = "border-start bi bi-x removeBt";
-                btItem.href = '#';
-                btItem.addEventListener('click', e => {
-                    e.preventDefault();
-                });
-                uiItem.appendChild(btItem);
-            }
-            tagListUi.appendChild(uiItem);
-        };
-        for (let i of fixedTagList)
-            createTagUiItem(i, true);
-        for (let i of tagList)
-            createTagUiItem(i, !writeAccess);
-        return tagListUi;
-    }
-
-    function _displayMediaFullPage(fileName, imgUrl, metaData, downloadLink, writeAccess) {
-        return new Promise(ok => {
-            document.getElementById("pch-fullPagePreviewContainer").classList.add("loading");
-            document.getElementById("pch-fullPageMedia-title").innerText = fileName;
-            document.getElementById("pch-fullPagePreview").onceLoaded = ok;
-            document.getElementById("pch-fullPagePreview").src = imgUrl;
-            document.getElementById("pch-fullPageDetail").innerText = "";
-            document.getElementById("pch-fullPageDetail").appendChild(displayMetas(Object.assign({}, metaData || {}), !writeAccess));
-            document.getElementById("pch-fullPageDetail").appendChild(displayTags(metaData?.fixedTags || [], metaData?.tags || [], writeAccess));
-            if (downloadLink) {
-                document.getElementById("pch-fullPageDetail-dlButton").classList.remove("hidden");
-                document.getElementById("pch-fullPageDetail-dlButton").dataset["link"] = downloadLink;
-            } else {
-                document.getElementById("pch-fullPageDetail-dlButton").classList.add("hidden");
-            }
-        });
-    }
-
-    function LoadPreviousMedia() {
-        let media = MediaStorage.Instance.previousMedia(fullPageMediaDisplayed);
-        if (media)
-            window.displayMediaFullPage(media);
-    }
-
-    function LoadNextMedia() {
-        let media = MediaStorage.Instance.nextMedia(fullPageMediaDisplayed);
-        if (media)
-            window.displayMediaFullPage(media);
-    }
-
-    function CloseFullpageMedia() {
-        document.getElementById("pch-fullPageMedia").classList.add("hidden");
-        fullPageMediaDisplayed = false;
-        history.pushState({}, '', '#');
-    }
-
-    window.displayMediaFullPage = function(mediaItem) {
-        document.getElementById("pch-fullPageMedia").classList.remove("hidden");
-        fullPageMediaDisplayed = mediaItem;
-        if (!mediaItem)
-            return _displayMediaFullPage("Error", null, {}, [], null, false);
-        let containerSize = document.getElementById("pch-fullPageMedia").getBoundingClientRect();
-        let requestSize = mediaItem.resize(containerSize.width, containerSize.height);
-        document.getElementById("pch-fullPagePreview").parentNode.style.maxWidth = "100%";
-        document.getElementById("pch-fullPagePreview").parentNode.style.maxHeight = "100%";
-        let meta = {
-            ...mediaItem.meta,
-            date: { type: 'date', value: mediaItem.date } || undefined,
-            filename: mediaItem.filename ? { type: 'string', value: mediaItem.fileName } : undefined,
-            fixedTags: mediaItem.fixedTags,
-            tags: mediaItem.tags
-        };
-        if (document.location.hash != `#${mediaItem.md5sum}`)
-            history.pushState({}, '', `#${mediaItem.md5sum}`);
-        return _displayMediaFullPage(mediaItem.fileName, `${mediaItem.thumbnail}?w=${requestSize.width}&h=${requestSize.height}&q=6`, meta, mediaItem.original, mediaItem.writeAccess);
-    }
-
-    document.getElementById("pch-fullPageMedia-closeBt")
-        .addEventListener("click", () => CloseFullpageMedia());
-    document.getElementById("pch-fullPagePreview").addEventListener("load", () => {
-        document.getElementById("pch-fullPagePreviewContainer").classList.remove("loading");
-        let domItem = document.getElementById("pch-fullPagePreview");
-        domItem.onceLoaded && domItem.onceLoaded();
-        domItem.onceLoaded = null;
-    });
-    document.getElementById('pch-fullPageDetail-dlButton').addEventListener("click", (evt) => {
-        if (!evt.target?.dataset?.link)
-            return;
-        let link = document.createElement('a');
-        link.target='_blank';
-        link.setAttribute("download", "");
-        link.href = evt.target.dataset.link;
-        link.click();
-    });
-    document.addEventListener("keyup", evt => {
-        lastKeyboardEvent = evt;
-    });
-    document.addEventListener("keydown", evt => {
-        lastKeyboardEvent = evt;
-        if (!fullPageMediaDisplayed)
-            return;
-        if (evt.keyCode === 37 || evt.keyCode === 38)
-            LoadPreviousMedia();
-        else if (evt.keyCode === 39 || evt.keyCode === 40)
-            LoadNextMedia();
-        else if (evt.keyCode === 27)
-            CloseFullpageMedia();
-        else {
-            console.log("Unregistered key event", evt.key, evt.keyCode);
-            return;
-        }
-        evt.preventDefault();
-    });
 });
 

+ 226 - 0
static/public/js/uiMediaFullpage.js

@@ -0,0 +1,226 @@
+
+$(() => {
+    function serializeFileSize(size) {
+        let units = [ 'o', 'Ko', 'Mo', 'Go', 'To' ];
+        let idx = 0;
+        while (size >= 800 && idx < units.length) {
+            ++idx;
+            size /= 1024;
+        }
+        size = Math.floor(size * 100) / 100;
+        return `${size} ${units[idx]}`;
+    }
+
+    function displayMeta(key, value, isRo) {
+        let li = document.createElement("li");
+        let type = value?.type || null;
+        let val = (value?.value !== undefined ? value.value : value);
+        if (!val && val !== '')
+            return null;
+        if (type === 'date')
+            val = val.toLocaleString();
+        if (["dimension", "height", "width"].indexOf(key) >= 0)
+            val += ' px';
+        if (["fileSize"].indexOf(key) >= 0)
+            val = serializeFileSize(val);
+        if (key == 'fNumber')
+            val = `f/ ${val}`;
+        let keyTranslate = {
+            fileSize: "File Size",
+            photochamberImport: "Photochamber Imported",
+            lensModel: "Lens Model",
+            exposureTimeStr: "Exposure Time"
+        };
+        let keySpan = document.createElement('span');
+        let valSpan = document.createElement('div');
+        li.classList.add("row");
+        keySpan.className = "metaKey col-xl-12 col-4";
+        valSpan.className = "metaVal col-xl-12 col-8";
+        let inputGroup = document.createElement('form');
+        valSpan.appendChild(inputGroup);
+        inputGroup.classList.add('input-group');
+        keySpan.innerText = (keyTranslate[key] || (key[0].toUpperCase()+key.substr(1))) + ':';
+        let valInput = document.createElement("input");
+        valInput.classList.add("form-control");
+        valInput.value = val;
+        valInput.disabled = isRo;
+        valInput.addEventListener('keyup', evt => evt.stopPropagation());
+        valInput.addEventListener('keydown', evt => evt.stopPropagation());
+        inputGroup.appendChild(valInput);
+        if (!isRo) {
+            let bt = document.createElement('button');
+            bt.className = 'btn btn-outline-secondary';
+            bt.type = 'button';
+            bt.innerHTML = '<i class="bi bi-pen"></i>';
+            inputGroup.appendChild(bt);
+            bt.addEventListener('click', () => MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed.md5sum, key, valInput.value));
+            inputGroup.addEventListener('submit', evt => {
+                evt.preventDefault();
+                MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed.md5sum, key, valInput.value);
+            });
+        }
+        li.appendChild(keySpan);
+        li.appendChild(valSpan);
+        return li;
+    }
+
+    function displayMetas(metaData, isRo) {
+        let metaList = document.createElement("ul");
+
+        metaData.libraryPath = null;
+
+        metaData.tags = metaData.fixedTags = null;
+
+        if (metaData.exposureTime && metaData.exposureTimeStr)
+            metaData.exposureTime = null;
+
+        if (metaData.date && metaData.dateTime)
+            metaData.dateTime = null;
+
+        if (metaData.height && metaData.width) {
+            metaData.dimension = { type: "string", value: `${metaData.width.value} x ${metaData.height.value}` }
+            metaData.height = metaData.width = null;
+        }
+
+        for (let i of [ "date", "dimension", "height", "width", "fileSize" ]) {
+            let metaItem = displayMeta(i, metaData[i], true);
+            metaItem && metaList.appendChild(metaItem);
+            metaData[i] = null;
+        }
+
+        for (let i of Object.keys(metaData).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
+            let metaItem = displayMeta(i, metaData[i], isRo || !MediaStorage.Instance.allMetaTypes[i]?.canWrite);
+            metaItem && metaList.appendChild(metaItem);
+        }
+
+        if (!isRo)
+            for (let i of ['geoCountry']) {
+                if (!metaData[i]) {
+                    let metaItem = displayMeta(i, "", false);
+                    metaItem && metaList.appendChild(metaItem);
+                }
+            }
+        return metaList;
+    }
+
+    function displayTags(fixedTagList, tagList, writeAccess) {
+        let tagListUi = document.createElement("ul");
+        tagListUi.className = "taglist";
+        let createTagUiItem = (i, roTag) => {
+            let uiItem = document.createElement("li");
+            uiItem.className = "badge text-bg-light";
+            let textItem = document.createElement("span");
+            textItem.textContent = i;
+            uiItem.appendChild(textItem);
+            if (!roTag) {
+                let btItem = document.createElement("a");
+                btItem.className = "border-start bi bi-x removeBt";
+                btItem.href = '#';
+                btItem.addEventListener('click', e => {
+                    e.preventDefault();
+                });
+                uiItem.appendChild(btItem);
+            }
+            tagListUi.appendChild(uiItem);
+        };
+        for (let i of fixedTagList)
+            createTagUiItem(i, true);
+        for (let i of tagList)
+            createTagUiItem(i, !writeAccess);
+        return tagListUi;
+    }
+
+    function _displayMediaFullPage(fileName, imgUrl, metaData, downloadLink, writeAccess) {
+        return new Promise(ok => {
+            document.getElementById("pch-fullPagePreviewContainer").classList.add("loading");
+            document.getElementById("pch-fullPageMedia-title").innerText = fileName;
+            document.getElementById("pch-fullPagePreview").onceLoaded = ok;
+            document.getElementById("pch-fullPagePreview").src = imgUrl;
+            document.getElementById("pch-fullPageDetail").innerText = "";
+            document.getElementById("pch-fullPageDetail").appendChild(displayMetas(Object.assign({}, metaData || {}), !writeAccess));
+            document.getElementById("pch-fullPageDetail").appendChild(displayTags(metaData?.fixedTags || [], metaData?.tags || [], writeAccess));
+            if (downloadLink) {
+                document.getElementById("pch-fullPageDetail-dlButton").classList.remove("hidden");
+                document.getElementById("pch-fullPageDetail-dlButton").dataset["link"] = downloadLink;
+            } else {
+                document.getElementById("pch-fullPageDetail-dlButton").classList.add("hidden");
+            }
+        });
+    }
+
+    function LoadPreviousMedia() {
+        let media = MediaStorage.Instance.previousMedia(fullPageMediaDisplayed);
+        if (media)
+            window.displayMediaFullPage(media);
+    }
+
+    function LoadNextMedia() {
+        let media = MediaStorage.Instance.nextMedia(fullPageMediaDisplayed);
+        if (media)
+            window.displayMediaFullPage(media);
+    }
+
+    function CloseFullpageMedia() {
+        document.getElementById("pch-fullPageMedia").classList.add("hidden");
+        fullPageMediaDisplayed = false;
+        history.pushState({}, '', '#');
+    }
+
+    window.displayMediaFullPage = function(mediaItem) {
+        document.getElementById("pch-fullPageMedia").classList.remove("hidden");
+        fullPageMediaDisplayed = mediaItem;
+        if (!mediaItem)
+            return _displayMediaFullPage("Error", null, {}, [], null, false);
+        let containerSize = document.getElementById("pch-fullPageMedia").getBoundingClientRect();
+        let requestSize = mediaItem.resize(containerSize.width, containerSize.height);
+        document.getElementById("pch-fullPagePreview").parentNode.style.maxWidth = "100%";
+        document.getElementById("pch-fullPagePreview").parentNode.style.maxHeight = "100%";
+        let meta = {
+            ...mediaItem.meta,
+            date: { type: 'date', value: mediaItem.date } || undefined,
+            filename: mediaItem.filename ? { type: 'string', value: mediaItem.fileName } : undefined,
+            fixedTags: mediaItem.fixedTags,
+            tags: mediaItem.tags
+        };
+        if (document.location.hash != `#${mediaItem.md5sum}`)
+            history.pushState({}, '', `#${mediaItem.md5sum}`);
+        return _displayMediaFullPage(mediaItem.fileName, `${mediaItem.thumbnail}?w=${requestSize.width}&h=${requestSize.height}&q=6`, meta, mediaItem.original, mediaItem.writeAccess);
+    }
+
+    document.getElementById("pch-fullPageMedia-closeBt")
+        .addEventListener("click", () => CloseFullpageMedia());
+    document.getElementById("pch-fullPagePreview").addEventListener("load", () => {
+        document.getElementById("pch-fullPagePreviewContainer").classList.remove("loading");
+        let domItem = document.getElementById("pch-fullPagePreview");
+        domItem.onceLoaded && domItem.onceLoaded();
+        domItem.onceLoaded = null;
+    });
+    document.getElementById('pch-fullPageDetail-dlButton').addEventListener("click", (evt) => {
+        if (!evt.target?.dataset?.link)
+            return;
+        let link = document.createElement('a');
+        link.target='_blank';
+        link.setAttribute("download", "");
+        link.href = evt.target.dataset.link;
+        link.click();
+    });
+    document.addEventListener("keyup", evt => {
+        lastKeyboardEvent = evt;
+    });
+    document.addEventListener("keydown", evt => {
+        lastKeyboardEvent = evt;
+        if (!fullPageMediaDisplayed)
+            return;
+        if (evt.keyCode === 37 || evt.keyCode === 38)
+            LoadPreviousMedia();
+        else if (evt.keyCode === 39 || evt.keyCode === 40)
+            LoadNextMedia();
+        else if (evt.keyCode === 27)
+            CloseFullpageMedia();
+        else {
+            console.log("Unregistered key event", evt.key, evt.keyCode);
+            return;
+        }
+        evt.preventDefault();
+    });
+});

+ 2 - 1
templates/_footer.js

@@ -1,13 +1,14 @@
 
 module.exports = `
 <script src="/public/js/jquery-3.6.1.min.js"></script>
-<script src="/public/bootstrap/bootstrap.min.js"></script>
+<script src="/public/bootstrap/bootstrap.bundle.min.js"></script>
 <script src="/public/js/popper.min.js"></script>
 <script src="/public/js/BsMultiSelect.min.js"></script>
 <script src="/public/js/taskQueue.js"></script>
 <script src="/public/js/medias.js"></script>
 <script src="/public/js/filters.js"></script>
 <script src="/public/js/uiMedia.js"></script>
+<script src="/public/js/uiMediaFullpage.js"></script>
 <script src="/public/js/uiAccess.js"></script>
 <script src="/public/js/uiFilter.js"></script>
 <script src="/public/js/chronology.js"></script>

+ 1 - 1
templates/_header.js

@@ -7,7 +7,7 @@ module.exports = `
 <link type="text/css" rel="stylesheet" href="/public/mdi/materialdesignicons.min.css"/>
 <link type="text/css" rel="stylesheet" href="/public/bootstrap/bootstrap.min.css"/>
 <link type="text/css" rel="stylesheet" href="/public/bootstrap/bootstrap-icons.min.css"/>
-<link type="text/css" rel="stylesheet" href="/public/css/BsMultiSelect.min.js"/>
+<link type="text/css" rel="stylesheet" href="/public/css/BsMultiSelect.min.css"/>
 <link type="text/css" rel="stylesheet" href="/public/css/style.css"/>
 </head>
 `;