uiMediaFullpage.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. $(() => {
  2. let fullPageMediaDisplayed = false;
  3. let fullPageMediaList = false;
  4. let metaListMap = {};
  5. function refreshOneMetaList(key, domNode) {
  6. domNode.textContent = "";
  7. let data = key === "Tags" ? Array.from(MediaStorage.Instance.allTags) : Array.from(MediaStorage.Instance.allMeta[key] || []);
  8. for (let i of data.sort())
  9. {
  10. let childDom = document.createElement("option");
  11. childDom.value = childDom.textContent = i;
  12. domNode.appendChild(childDom);
  13. }
  14. }
  15. function buildMetaDatalistes(key, datalistes) {
  16. let uuid = "METALIST_"+crypto.randomUUID();
  17. let list = document.createElement("datalist");
  18. list.id = uuid;
  19. datalistes.push(list);
  20. refreshOneMetaList(key, list);
  21. metaListMap[key] = uuid;
  22. return uuid;
  23. }
  24. function refreshMetaDatalistes() {
  25. for (let i of Object.keys(metaListMap)) {
  26. refreshOneMetaList(i, document.getElementById(metaListMap[i]));
  27. }
  28. }
  29. function serializeFileSize(size) {
  30. let units = [ 'o', 'Ko', 'Mo', 'Go', 'To' ];
  31. let idx = 0;
  32. while (size >= 800 && idx < units.length) {
  33. ++idx;
  34. size /= 1024;
  35. }
  36. size = Math.floor(size * 100) / 100;
  37. return `${size} ${units[idx]}`;
  38. }
  39. function displayMeta(key, value, isRo, datalistes) {
  40. let li = document.createElement("li");
  41. let type = value?.type || null;
  42. let val = (value?.value !== undefined ? value.value : value);
  43. if (!val && val !== '')
  44. return null;
  45. if (type === 'date')
  46. val = val.toLocaleString();
  47. if (["dimension", "height", "width"].indexOf(key) >= 0)
  48. val += ' px';
  49. if (["fileSize"].indexOf(key) >= 0)
  50. val = serializeFileSize(val);
  51. if (key == 'fNumber')
  52. val = `f/ ${val}`;
  53. let keyTranslate = {
  54. fileSize: "File Size",
  55. photochamberImport: "Photochamber Imported",
  56. lensModel: "Lens Model",
  57. exposureTimeStr: "Exposure Time"
  58. };
  59. let keySpan = document.createElement('span');
  60. let valSpan = document.createElement('div');
  61. li.classList.add("row");
  62. keySpan.className = "metaKey col-xl-12 col-4";
  63. valSpan.className = "metaVal col-xl-12 col-8";
  64. let inputGroup = document.createElement('form');
  65. valSpan.appendChild(inputGroup);
  66. inputGroup.classList.add('input-group');
  67. keySpan.innerText = (keyTranslate[key] || (key[0].toUpperCase()+key.substr(1))) + ':';
  68. let valInput = document.createElement("input");
  69. valInput.classList.add("form-control");
  70. valInput.value = val;
  71. valInput.disabled = isRo;
  72. valInput.addEventListener('keyup', evt => evt.stopPropagation());
  73. valInput.addEventListener('keydown', evt => evt.stopPropagation());
  74. inputGroup.appendChild(valInput);
  75. if (!isRo) {
  76. let bt = document.createElement('button');
  77. bt.className = 'btn btn-outline-secondary';
  78. bt.type = 'button';
  79. bt.innerHTML = '<i class="bi bi-pen"></i>';
  80. inputGroup.appendChild(bt);
  81. valInput.setAttribute("list", buildMetaDatalistes(key, datalistes));
  82. bt.addEventListener('click', async () => {
  83. await MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed?.fixedSum || fullPageMediaList, key, valInput.value)
  84. reloadCurrentMedia();
  85. });
  86. inputGroup.addEventListener('submit', async evt => {
  87. evt.preventDefault();
  88. await MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed?.fixedSum || fullPageMediaList, key, valInput.value);
  89. reloadCurrentMedia();
  90. });
  91. }
  92. li.appendChild(keySpan);
  93. li.appendChild(valSpan);
  94. return li;
  95. }
  96. let latLngTimeo = null;
  97. function setLatLngWithDelay(medias, lat, lng) {
  98. if (latLngTimeo)
  99. clearTimeout(latLngTimeo);
  100. latLngTimeo = setTimeout(async () => {
  101. await MediaStorage.Instance.setMetaValue(medias.map(x => x.fixedSum), 'gpsLocation', JSON.stringify([lat, lng]));
  102. latLngTimeo = null;
  103. reloadCurrentMedia();
  104. }, 500);
  105. }
  106. function displayMap(media, geo, isRo) {
  107. let jsonGeo = geo;
  108. let marker;
  109. let searchMarker;
  110. let searchPopup;
  111. let createMarker = (latLng) => {
  112. if (marker)
  113. return marker;
  114. marker = L.marker(latLng, { draggable: true, autoPan: true });
  115. if (!isRo)
  116. marker.addEventListener('dragend', e => {
  117. let latLng = e.target?.getLatLng();
  118. if (!latLng || !latLng.lat || !latLng.lng)
  119. return;
  120. setLatLngWithDelay(media, latLng.lat, latLng.lng);
  121. });
  122. return marker;
  123. };
  124. try {
  125. geo = geo && JSON.parse(geo);
  126. }
  127. catch(err) { return null; }
  128. let outerHTML = document.createElement("li");
  129. outerHTML.className = "row";
  130. let container = document.createElement("div");
  131. container.className = "leaflet-container container";
  132. outerHTML.appendChild(container);
  133. let innerHTML = document.createElement("div");
  134. container.appendChild(innerHTML);
  135. let map = L.map(innerHTML, { scrollWheelZoom: false });
  136. L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
  137. attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  138. }).addTo(map);
  139. if (!isRo) {
  140. L.Control.geocoder({defaultMarkGeocode: false}).on('markgeocode', e => {
  141. let pos = e?.geocode?.center;
  142. if (!pos)
  143. return;
  144. if (!searchMarker)
  145. searchMarker = L.marker(pos).addTo(map);
  146. else
  147. searchMarker.setLatLng(pos);
  148. searchMarker.bindPopup("<div class='container'><div class='row'>" +(e.geocode.html || e.geocode.name) + "</div><div class='row'><button class='btn btn-primary'>Set</button></div></div>").openPopup();
  149. searchMarker._popup._container.querySelector("button").addEventListener('click', () => {
  150. setLatLngWithDelay(media, pos.lat, pos.lng);
  151. });
  152. map.setView(pos, 13);
  153. }).addTo(map);
  154. map.addEventListener("contextmenu", evt => {
  155. if (!marker)
  156. createMarker(evt.latlng).addTo(map);
  157. else
  158. marker.setLatLng(evt.latlng);
  159. setLatLngWithDelay(media, evt.latlng.lat, evt.latlng.lng);
  160. });
  161. }
  162. if (geo) {
  163. map.setView(geo, 13);
  164. createMarker(geo).addTo(map);
  165. }
  166. else
  167. map.setView(L.latLng(45, 2), 5);
  168. setTimeout(function () {
  169. map.invalidateSize();
  170. }, 0);
  171. if (jsonGeo) {
  172. let a = document.createElement("a");
  173. a.href = `https://www.openstreetmap.org/?mlat=${geo[0]}&mlon=${geo[1]}`;
  174. a.target = "_blank";
  175. a.innerHTML = jsonGeo;
  176. container.appendChild(a);
  177. }
  178. return outerHTML;
  179. }
  180. function displayMetas(media, metaData, isRo, datalistes) {
  181. let metaList = document.createElement("ul");
  182. metaData.libraryPath = null;
  183. metaData.tags = metaData.fixedTags = null;
  184. if (metaData.exposureTime && metaData.exposureTimeStr)
  185. metaData.exposureTime = null;
  186. if (metaData.date && metaData.dateTime)
  187. metaData.dateTime = null;
  188. if (metaData.height && metaData.width) {
  189. metaData.dimension = { type: "string", value: `${metaData.width.value} x ${metaData.height.value}` }
  190. metaData.height = metaData.width = null;
  191. }
  192. for (let i of [ "date", "dimension", "height", "width", "fileSize" ]) {
  193. let metaItem = displayMeta(i, metaData[i], true, datalistes);
  194. metaItem && metaList.appendChild(metaItem);
  195. metaData[i] = null;
  196. }
  197. for (let i of Object.keys(metaData).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
  198. if (i === 'gpsLocation') {
  199. let dom = displayMap(Array.isArray(media) ? media : [media], metaData[i].value, isRo);
  200. dom && metaList.appendChild(dom);
  201. continue;
  202. }
  203. let metaItem = displayMeta(i, metaData[i], isRo || !MediaStorage.Instance.allMetaTypes[i]?.canWrite, datalistes);
  204. metaItem && metaList.appendChild(metaItem);
  205. }
  206. if (!isRo && media) {
  207. for (let i of ['geoCountry', 'geoCity', 'geoAdmin']) {
  208. if (!metaData[i]) {
  209. let metaItem = displayMeta(i, "", false, datalistes);
  210. metaItem && metaList.appendChild(metaItem);
  211. }
  212. }
  213. if (!metaData['gpsLocation']) {
  214. let dom = displayMap(Array.isArray(media) ? media : [media], null, isRo);
  215. dom && metaList.appendChild(dom);
  216. }
  217. }
  218. return metaList;
  219. }
  220. function reloadCurrentMedia() {
  221. fullPageMediaDisplayed && window.displayMediaFullPage(MediaStorage.Instance.getMediaLocal(fullPageMediaDisplayed.fixedSum));
  222. }
  223. function displayTags(fixedTagList, tagList, writeAccess, datalistes) {
  224. let tagListUi = document.createElement("ul");
  225. tagListUi.className = "taglist";
  226. let createTagUiItem = (i, roTag) => {
  227. let uiItem = document.createElement("li");
  228. uiItem.className = "badge text-bg-light";
  229. let textItem = document.createElement("span");
  230. textItem.textContent = i;
  231. uiItem.appendChild(textItem);
  232. if (!roTag) {
  233. let btItem = document.createElement("a");
  234. btItem.className = "border-start bi bi-x removeBt";
  235. btItem.href = '#';
  236. btItem.addEventListener('click', async e => {
  237. e.preventDefault();
  238. await MediaStorage.Instance.removeTag(fullPageMediaDisplayed?.fixedSum || fullPageMediaList, i);
  239. reloadCurrentMedia();
  240. });
  241. uiItem.appendChild(btItem);
  242. }
  243. tagListUi.appendChild(uiItem);
  244. };
  245. for (let i of fixedTagList)
  246. createTagUiItem(i, true);
  247. for (let i of tagList)
  248. createTagUiItem(i, !writeAccess);
  249. if (writeAccess) {
  250. let inputGroup = document.createElement('form');
  251. tagListUi.appendChild(inputGroup);
  252. inputGroup.classList.add('input-group');
  253. let valInput = document.createElement("input");
  254. valInput.classList.add("form-control");
  255. valInput.addEventListener('keyup', evt => evt.stopPropagation());
  256. valInput.addEventListener('keydown', evt => evt.stopPropagation());
  257. valInput.setAttribute("list", buildMetaDatalistes("Tags", datalistes));
  258. inputGroup.appendChild(valInput);
  259. let bt = document.createElement('button');
  260. bt.className = 'btn btn-outline-secondary';
  261. bt.type = 'button';
  262. bt.innerHTML = '<i class="bi bi-tags"></i>';
  263. inputGroup.appendChild(bt);
  264. bt.addEventListener('click', async () => { await MediaStorage.Instance.addTag(fullPageMediaDisplayed?.fixedSum || fullPageMediaList, valInput.value); reloadCurrentMedia(); });
  265. inputGroup.addEventListener('submit', async evt => {
  266. evt.preventDefault();
  267. await MediaStorage.Instance.addTag(fullPageMediaDisplayed?.fixedSum || fullPageMediaList, valInput.value);
  268. reloadCurrentMedia();
  269. });
  270. }
  271. return tagListUi;
  272. }
  273. function displayDownloadBt(downloadLink) {
  274. let bt = document.createElement("button");
  275. bt.type = "button";
  276. bt.className = "btn btn-primary";
  277. bt.textContent = "Download";
  278. bt.addEventListener("click", (evt) => {
  279. let link = document.createElement('a');
  280. link.target='_blank';
  281. link.setAttribute("download", "");
  282. link.href = downloadLink;
  283. link.click();
  284. });
  285. return bt;
  286. }
  287. function displayRemoveButton(ids) {
  288. let bt = document.createElement("button");
  289. bt.type = "button";
  290. bt.className = "btn btn-danger";
  291. bt.textContent = ids.length > 1 ? "Remove files" : "Remove file";
  292. bt.addEventListener("click", async evt => {
  293. if (window.confirm("File will be removed from the server. Are you sure ?")) {
  294. await MediaStorage.Instance.remoteRemove(ids);
  295. CloseFullpageMedia();
  296. }
  297. });
  298. return bt;
  299. }
  300. function displayImg(medias) {
  301. if (!Array.isArray(medias))
  302. medias = [ medias ];
  303. if (medias.length > 1) {
  304. $("#pch-fullPagePreview > a").removeClass("hidden");
  305. $("#pch-fullPagePreview > .carousel-indicators").removeClass("hidden");
  306. } else {
  307. $("#pch-fullPagePreview > a").addClass("hidden");
  308. $("#pch-fullPagePreview > .carousel-indicators").addClass("hidden");
  309. }
  310. const containerSize = document.getElementById("pch-fullPageMedia").getBoundingClientRect();
  311. let container = document.querySelector("#pch-fullPagePreview .carousel-inner");
  312. container.textContent = "";
  313. let carouselIndicators = document.querySelector("#pch-fullPagePreview .carousel-indicators");
  314. carouselIndicators.textContent = "";
  315. return Promise.allSettled(medias.map((media, index) => new Promise(ok => {
  316. let item = document.createElement("div");
  317. item.className = "carousel-item";
  318. let img = document.createElement("img");
  319. let requestSize = media.resize(containerSize.width, containerSize.height);
  320. const requestSizeQuery = requestSize ? `&w=${requestSize.width}&h=${requestSize.height}` : "";
  321. img.src = `${media.thumbnail}?q=6${requestSizeQuery}`;
  322. img.addEventListener("load", () => ok());
  323. item.appendChild(img);
  324. container.appendChild(item);
  325. let carouselIndicator = document.createElement("button");
  326. carouselIndicator.type = "button";
  327. carouselIndicator.dataset.bsTarget = "#pch-fullPagePreview";
  328. carouselIndicator.dataset.bsSlideTo = index;
  329. if (!index) {
  330. item.classList.add("active");
  331. carouselIndicator.classList.add("active");
  332. }
  333. carouselIndicators.appendChild(carouselIndicator);
  334. ++index;
  335. }))).finally(() => {
  336. document.getElementById("pch-fullPagePreviewContainer").classList.remove("loading");
  337. if (medias.length > 1)
  338. $(container.parentElement).carousel();
  339. });
  340. }
  341. function _displayMediaFullPage(media, fileName, imgUrl, metaData, downloadLink, fileIds, writeAccess) {
  342. document.getElementById("pch-fullPagePreviewContainer").classList.add("loading");
  343. document.getElementById("pch-fullPageMedia-title").innerText = fileName;
  344. let datalistes = [];
  345. metaListMap = {};
  346. document.getElementById("pch-fullPageDetail").innerText = "";
  347. if (downloadLink)
  348. document.getElementById("pch-fullPageDetail").appendChild(displayDownloadBt(downloadLink));
  349. if (fileIds && writeAccess)
  350. document.getElementById("pch-fullPageDetail").appendChild(displayRemoveButton(fileIds));
  351. document.getElementById("pch-fullPageDetail").appendChild(displayTags(metaData?.fixedTags || [], metaData?.tags || [], writeAccess, datalistes));
  352. document.getElementById("pch-fullPageDetail").appendChild(displayMetas(media, Object.assign({}, metaData || {}), !writeAccess, datalistes));
  353. document.querySelector("#pch-fullPagePreview .carousel-inner").textContent = "";
  354. $("#pch-fullPagePreview.carousel").carousel("dispose");
  355. {
  356. let datalistParent = document.getElementById("datalistes");
  357. datalistParent.textContent = "";
  358. for (let i of datalistes)
  359. datalistParent.appendChild(i);
  360. }
  361. return displayImg(media, imgUrl);
  362. }
  363. function LoadPreviousMedia() {
  364. let i = fullPageMediaDisplayed;
  365. if (!i) return;
  366. while (i = MediaStorage.Instance.previousMedia(i)) {
  367. if (window.FilterManager.match(i)) {
  368. window.displayMediaFullPage(i);
  369. break;
  370. }
  371. }
  372. }
  373. function LoadNextMedia() {
  374. let i = fullPageMediaDisplayed;
  375. if (!i) return;
  376. while (i = MediaStorage.Instance.nextMedia(i)) {
  377. if (window.FilterManager.match(i)) {
  378. window.displayMediaFullPage(i);
  379. break;
  380. }
  381. }
  382. }
  383. function CloseFullpageMedia() {
  384. if (fullPageMediaDisplayed !== false || fullPageMediaList)
  385. document.body.classList.remove("overlay-visible");
  386. document.getElementById("pch-fullPageMedia").classList.add("hidden");
  387. fullPageMediaDisplayed = false;
  388. history.pushState({}, '', '#');
  389. document.Title.pop();
  390. metaListMap = {};
  391. document.getElementById("datalistes").textContent = "";
  392. }
  393. window.displayMediaFullPage = function(mediaItem) {
  394. document.getElementById("pch-fullPageMedia").classList.remove("hidden");
  395. document.getElementById("pch-fullPageMedia").classList.remove("multiple");
  396. if (fullPageMediaDisplayed)
  397. document.Title.replaceTitle(mediaItem.fileName);
  398. else
  399. document.Title.pushTitle(mediaItem.fileName);
  400. fullPageMediaDisplayed = mediaItem ?? null;
  401. document.body.classList.add("overlay-visible");
  402. if (!mediaItem)
  403. return _displayMediaFullPage(null, "Error", null, {}, null, null, false);
  404. let meta = {
  405. ...mediaItem.meta,
  406. date: { type: 'date', value: mediaItem.getDate() } || undefined,
  407. filename: mediaItem.filename ? { type: 'string', value: mediaItem.fileName } : undefined,
  408. fixedTags: mediaItem.fixedTags,
  409. tags: mediaItem.tags
  410. };
  411. if (document.location.hash != `#${mediaItem.fixedSum}`)
  412. history.pushState({}, '', `#${mediaItem.fixedSum}`);
  413. return _displayMediaFullPage(mediaItem, mediaItem.fileName, "", meta, `${mediaItem.original}?trim`, [mediaItem.fixedSum], mediaItem.writeAccess);
  414. }
  415. function aggregateMetas(medias) {
  416. let meta = medias.reduce((acc, x) => {
  417. for (let key in x.meta) {
  418. acc[key] = acc[key] || x.meta[key];
  419. acc[key] = acc[key].value != x.meta[key].value ? "(multiple)" : x.meta[key];
  420. }
  421. return acc;
  422. }, {});
  423. delete meta.dateTime;
  424. if (!Number.isInteger(meta.height) || !Number.isInteger(meta.width)) {
  425. delete meta.height;
  426. delete meta.width;
  427. }
  428. return meta;
  429. }
  430. window.displayMultipleMediaFullPage = function(medias) {
  431. const title = "Multiple edit"; // FIXME lang ?
  432. fullPageMediaList = Array.from(new Set(medias.map(x => x.fixedSum)));
  433. document.getElementById("pch-fullPageMedia").classList.remove("hidden");
  434. document.getElementById("pch-fullPageMedia").classList.add("multiple");
  435. document.Title.pushTitle(title);
  436. document.body.classList.add("overlay-visible");
  437. let meta = {
  438. ...aggregateMetas(medias),
  439. fixedTags: medias.reduce((acc, x) => { x.fixedTags.forEach(tag => acc.add(tag)) ; return acc; }, new Set()),
  440. tags: medias.reduce((acc, x) => { x.tags.forEach(tag => acc.add(tag)) ; return acc; }, new Set()),
  441. };
  442. return _displayMediaFullPage(medias, title, "", meta, null, medias.map(x => x.fixedSum), true);
  443. }
  444. document.getElementById("pch-fullPageMedia-closeBt")
  445. .addEventListener("click", () => CloseFullpageMedia());
  446. document.addEventListener("keydown", evt => {
  447. if (!fullPageMediaDisplayed)
  448. return;
  449. if (evt.keyCode === 37 || evt.keyCode === 38)
  450. LoadPreviousMedia();
  451. else if (evt.keyCode === 39 || evt.keyCode === 40)
  452. LoadNextMedia();
  453. });
  454. document.onClosePopinRequested(() => { CloseFullpageMedia(); });
  455. document.UiFullPageRefreshMetaDatalistes = refreshMetaDatalistes;
  456. });