Browse Source

Refs #4 Write meta

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

+ 11 - 1
model/mediaService.js

@@ -21,6 +21,12 @@ function MediaStruct(i) {
 MediaStruct.prototype.pushMeta = function(key, value) {
     if (key && value && !this.meta[key]) {
         let type = '';
+        let roTypes = [
+            'photochamberImport', 'height', 'width', 'iso', 'focal',
+            'fNumber', 'exposureTime', 'camera', 'lensModel',
+            'exposureTimeStr', 'libraryPath', 'compression',
+            'software', 'fileSize', 'geoHash'
+        ];
         if (['photochamberImport', 'dateTime'].indexOf(key) >= 0)
             type = 'date';
         else if (['height', 'width', 'iso', 'focal', 'fNumber', 'exposureTime'].indexOf(key) >= 0)
@@ -33,7 +39,11 @@ MediaStruct.prototype.pushMeta = function(key, value) {
         else if (['fileSize'].indexOf(key) >= 0)
             type = 'octet';
         else console.log(`Unknown meta type ${key} (${value})`);
-        this.meta[key] = { type: type, value: value };
+        this.meta[key] = {
+            type: type,
+            canWrite: roTypes.indexOf(key) === -1,
+            value: value
+        };
     }
 }
 

+ 13 - 1
router/api.js

@@ -4,7 +4,8 @@ const fs = require('fs');
 const Path = require('path');
 const Security = require('../src/security.js');
 const MediaService = require('../model/mediaService.js');
-const { AccessModel, ACCESS_TYPE } = require('../model/access.js');
+const MediaFileMetaModel = require('../model/mediaItemMeta.js').MediaFileMetaModel;
+const { AccessModel, ACCESS_TYPE, ACCESS_GRANT } = require('../model/access.js');
 
 module.exports = { register: app => {
     app.router.get("/api/access/list", (req, res) => {
@@ -26,6 +27,17 @@ module.exports = { register: app => {
         const result = Security.removeFromSession(req, req.params.id);
         app.routerUtils.jsonResponse(res, result);
     });
+    app.router.patch("/api/media/:id/meta/:key", async (req, res) => {
+        app.routerUtils.onApiRequest(req, res);
+        if (!req.params.id ||!req.params.key || !req.body)
+            return app.routerUtils.onBadRequest(res);
+        let data = await MediaService.fetchOne(app, req.params.id, req.sessionObj?.accessList);
+        if (!data || data.accessType !== ACCESS_GRANT.write)
+            return app.routerUtils.onPageNotFound(res);
+        let newMediaItemMedia = new MediaFileMetaModel(data.md5sum, req.params.key, req.body.value, false);
+        await app.databaseHelper.upsertOne(newMediaItemMedia);
+        res.end();
+    });
     app.router.get("/api/media/list", async (req, res) => {
         app.routerUtils.onApiRequest(req, res);
         let first = undefined,

+ 4 - 1
static/public/css/style.css

@@ -130,7 +130,10 @@
 
 #pch-fullPageDetail > ul .metaKey {
     text-decoration: underline;
-    margin-right: .4em;
+}
+
+#pch-fullPageDetail > ul .metaVal {
+    display: inline-block;
 }
 
 #pch-fullPageMedia .taglist {

+ 1 - 1
static/public/js/common.js

@@ -27,7 +27,7 @@ $(() => {
                 let chronology = window.chronology.isInitialized() ? "" : "&chronology"
                 let oldest = (MediaStorage.Instance.oldest?.date?.getTime() || 0);
                 let oldestArg = oldest ? `&from=${oldest}` : "";
-                $.get(`/api/media/list?count=75${chronology}${oldestArg}`, data => {
+                $.get(`/api/media/list?count=100${chronology}${oldestArg}`, data => {
                     MediaStorage.Instance.pushAll(data.data.map(i => new Media(i)));
                     if (data.first || data.last)
                         window.chronology.rebuildRange(data.first, data.last);

+ 25 - 2
static/public/js/medias.js

@@ -13,7 +13,6 @@ class Media {
         this.original = `/api/media/original/${data.md5sum}`;
         this.ui = null;
 
-
         this.tags = this.tags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set());
         this.fixedTags = this.fixedTags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set());
 
@@ -74,7 +73,7 @@ class MediaStorage extends EventTarget
             this.allMeta[metaKey] = new Set();
         this.allMeta[metaKey].add(metaVal.value);
         if (!this.allMetaTypes[metaKey])
-            this.allMetaTypes[metaKey] = { type: metaVal.type, canBeEmpty: !!this.medias.length };
+            this.allMetaTypes[metaKey] = { type: metaVal.type, canBeEmpty: !!this.medias.length, canWrite: metaVal.canWrite };
     }
 
     #pushTag(tag, first) {
@@ -159,6 +158,30 @@ class MediaStorage extends EventTarget
             return media;
         return await tryLoadMedia(md5sum);
     }
+
+    setMetaValue(md5sum, key, value) {
+        LoadingTasks.push(() => {
+            return new Promise(ok => {
+                let media = this.medias.find(x => x.md5sum === md5sum);
+                if (!media || !media.writeAccess)
+                    return ok(false);
+                $.ajax({
+                    url: `/api/media/${encodeURIComponent(md5sum)}/meta/${encodeURIComponent(key)}`,
+                    type: "PATCH",
+                    data: { value },
+                    success: () => {
+                        let meta = media.meta[key] || { type: 'string', value: value, canWrite: true };
+                        meta.value = value;
+                        this.#pushMeta(key, meta);
+                        media.meta[key] = meta;
+                        window.ReloadFilters(this);
+                        ok(true);
+                    },
+                    error: err => ok(false),
+                });
+            });
+        });
+    }
 }
 
 MediaStorage.Instance = new MediaStorage();

+ 1 - 0
static/public/js/uiFilter.js

@@ -31,6 +31,7 @@ $(() => {
         };
 
         let container = document.getElementById('pch-filterbar');
+        container.textContent = '';
         for (let i of Object.keys(mediaManager.allMetaTypes).filter(i => mediaManager.allMetaTypes[i].type === 'string')) {
             let filterUi = buildFilterBar(i, mediaManager.allMetaTypes[i].canBeEmpty, Array.from(mediaManager.allMeta[i]).sort());
             container.appendChild(filterUi.content);

+ 42 - 13
static/public/js/uiMedia.js

@@ -149,11 +149,11 @@ $(() => {
         return `${size} ${units[idx]}`;
     }
 
-    function displayMeta(key, value) {
+    function displayMeta(key, value, isRo) {
         let li = document.createElement("li");
         let type = value?.type || null;
-        let val = (value?.value ? value.value : value) || null;
-        if (!val)
+        let val = (value?.value ? value.value : value);
+        if (!val && val !== '')
             return null;
         if (type === 'date')
             val = val.toLocaleString();
@@ -170,17 +170,39 @@ $(() => {
             exposureTimeStr: "Exposure Time"
         };
         let keySpan = document.createElement('span');
-        let valSpan = document.createElement('span');
-        keySpan.classList.add("metaKey");
-        valSpan.classList.add("metaVal");
+        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))) + ':';
-        valSpan.innerText = val;
+        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) {
+    function displayMetas(metaData, isRo) {
         let metaList = document.createElement("ul");
 
         metaData.libraryPath = null;
@@ -199,16 +221,23 @@ $(() => {
         }
 
         for (let i of [ "date", "dimension", "height", "width", "fileSize" ]) {
-            let metaItem = displayMeta(i, metaData[i]);
+            let metaItem = displayMeta(i, metaData[i], true);
             metaItem && metaList.appendChild(metaItem);
             metaData[i] = null;
         }
 
-        // FIXME sort and filter
-        for (let i in metaData) {
-            let metaItem = displayMeta(i, metaData[i]);
+        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;
     }
 
@@ -246,7 +275,7 @@ $(() => {
             document.getElementById("pch-fullPagePreview").onceLoaded = ok;
             document.getElementById("pch-fullPagePreview").src = imgUrl;
             document.getElementById("pch-fullPageDetail").innerText = "";
-            document.getElementById("pch-fullPageDetail").appendChild(displayMetas(Object.create(metaData || {})));
+            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");

+ 1 - 1
templates/_fullPageMedia.js

@@ -19,7 +19,7 @@ module.exports = `
                     <div>
                         <button type="button" class="btn btn-primary" id="pch-fullPageDetail-dlButton">Download</button>
                     </div>
-                    <div id="pch-fullPageDetail"></div>
+                    <div id="pch-fullPageDetail" class="container-fluid"></div>
                 </div>
             </div>
         </div>