1
0

uiMediaFullpage.js 18 KB

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