1
0

uiMedia.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. $(() => {
  2. var fullPageMediaDisplayed = false;
  3. var selectedThumbnails = [];
  4. var lastKeyboardEvent = null;
  5. var lastSelection = null;
  6. function onItemSelected(mediaItem) {
  7. document.getElementById("pch-mediaList").classList.add("selection");
  8. }
  9. function onItemDeselected(mediaItem) {
  10. if (!selectedThumbnails.length)
  11. document.getElementById("pch-mediaList").classList.remove("selection");
  12. }
  13. function buildThumbnail(mediaItem) {
  14. if (mediaItem.ui)
  15. return mediaItem.ui;
  16. let checkbox = document.createElement("input");
  17. let editButton = document.createElement("button");
  18. let img = document.createElement("img");
  19. let container = document.createElement("li");
  20. let loadingImg = document.createElement("div");
  21. checkbox.type = "checkbox";
  22. editButton.type = "button";
  23. if (!mediaItem.writeAccess)
  24. editButton.classList.add("hidden");
  25. let editButtonSpan = document.createElement("span");
  26. editButtonSpan.className = "bi bi-pen";
  27. editButton.appendChild(editButtonSpan);
  28. container.classList.add("pch-image");
  29. container.classList.add("loading");
  30. loadingImg.classList.add("spinner");
  31. loadingImg.innerHTML = "<span class='spinner-grow'></span>";
  32. container.dataset.md5sum = mediaItem.md5sum;
  33. img.loading = "lazy";
  34. let requestSize = mediaItem.resize(450, 450);
  35. img.src = `${mediaItem.thumbnail}?w=${requestSize.width}&h=${requestSize.height}&q=4`;
  36. img.classList.add("img-fluid");
  37. img.classList.add("img-thumbnail");
  38. img.addEventListener("load", () => {
  39. container.classList.remove("loading");
  40. container.classList.remove("spinner-grow");
  41. });
  42. container.style.width = `${requestSize.width}px`;
  43. container.appendChild(loadingImg);
  44. container.appendChild(img);
  45. container.appendChild(checkbox);
  46. container.appendChild(editButton);
  47. let setSelectionCheckboxValue = function(media, value) {
  48. let checked = media.ui.checkbox.checked;
  49. let indexInSelection = selectedThumbnails.indexOf(media.md5sum);
  50. if (checked && indexInSelection < 0) {
  51. selectedThumbnails.push(media.md5sum);
  52. onItemSelected(media);
  53. return true;
  54. } else if (!checked && indexInSelection >= 0) {
  55. selectedThumbnails.splice(indexInSelection, 1);
  56. onItemDeselected(media);
  57. return true;
  58. }
  59. return false;
  60. }
  61. let cascadeSetSelectionCheckboxValue = function(value) {
  62. if (!setSelectionCheckboxValue(mediaItem, value))
  63. return;
  64. if (lastKeyboardEvent?.shiftKey && lastSelection) {
  65. let _lastKeyboardEvent = lastKeyboardEvent;
  66. lastKeyboardEvent = null;
  67. for (let i of MediaStorage.Instance.getMediaBetween(lastSelection, mediaItem)) {
  68. if (i === mediaItem)
  69. continue;
  70. i.ui.checkbox.setAttribute("checked", value);
  71. i.ui.checkbox.checked = value;
  72. setSelectionCheckboxValue(i, value);
  73. }
  74. lastKeyboardEvent = _lastKeyboardEvent;
  75. }
  76. lastSelection = mediaItem;
  77. }
  78. editButton.addEventListener("click", e => {
  79. e.stopPropagation();
  80. if (!checkbox.checked) {
  81. checkbox.checked = true;
  82. setSelectionCheckboxValue(mediaItem, true);
  83. }
  84. let sel = selectedThumbnails.map(x => MediaStorage.Instance.getMediaLocal(x)).filter(x => x.writeAccess);
  85. if (sel.length === 1 && sel[0].md5sum === mediaItem.md5sum) {
  86. checkbox.checked = false;
  87. setSelectionCheckboxValue(mediaItem, false);
  88. document.location.hash = mediaItem.md5sum;
  89. return;
  90. }
  91. console.log(sel);
  92. });
  93. container.addEventListener("click", () => {
  94. if (selectedThumbnails.length || lastKeyboardEvent?.ctrlKey) {
  95. let value = !checkbox.checked;
  96. checkbox.setAttribute("checked", value);
  97. checkbox.checked = value;
  98. cascadeSetSelectionCheckboxValue(value);
  99. return;
  100. }
  101. document.location.hash = mediaItem.md5sum;
  102. });
  103. checkbox.addEventListener("click", evt => {
  104. evt.stopPropagation();
  105. });
  106. checkbox.addEventListener("change", evt => {
  107. cascadeSetSelectionCheckboxValue(checkbox.checked);
  108. });
  109. return mediaItem.ui = {
  110. root: container,
  111. img: img,
  112. checkbox: checkbox
  113. };
  114. }
  115. function buildYear(date) {
  116. let result = document.createElement('h3');
  117. result.textContent = date.getUTCFullYear();
  118. return result;
  119. }
  120. function buildMonth(date) {
  121. let result = document.createElement('h4');
  122. result.textContent = date.toLocaleString('default', { month: 'long' });
  123. return result;
  124. }
  125. function redraw(container, media) {
  126. buildThumbnail(media);
  127. if (!media.ui)
  128. return;
  129. let yearUpdated = !container.dataset.lastItemYear || container.dataset.lastItemYear != media.date.getUTCFullYear();
  130. if (yearUpdated) {
  131. container.appendChild(buildYear(media.date));
  132. container.dataset.lastItemYear = media.date.getUTCFullYear();
  133. }
  134. if (yearUpdated || container.dataset.lastItemMonth === undefined || container.dataset.lastItemMonth != media.date.getUTCMonth()) {
  135. container.appendChild(buildMonth(media.date));
  136. container.dataset.lastItemMonth = media.date.getUTCMonth();
  137. }
  138. container.appendChild(media.ui.root);
  139. }
  140. MediaStorage.Instance.addEventListener("rebuildMedia", (evt) => {
  141. let newContainer = document.getElementById('pch-mediaList');
  142. newContainer.textContent = '';
  143. newContainer.dataset.lastItemYear = null;
  144. newContainer.dataset.lastItemMonth = null;
  145. for (let i of MediaStorage.Instance.medias)
  146. if (window.FilterManager.match(i))
  147. redraw(newContainer, i);
  148. });
  149. MediaStorage.Instance.addEventListener("newMedia", (evt) => {
  150. let container = document.getElementById('pch-mediaList');
  151. if (window.FilterManager.match(evt.detail))
  152. redraw(container, evt.detail);
  153. });
  154. function serializeFileSize(size) {
  155. let units = [ 'o', 'Ko', 'Mo', 'Go', 'To' ];
  156. let idx = 0;
  157. while (size >= 800 && idx < units.length) {
  158. ++idx;
  159. size /= 1024;
  160. }
  161. size = Math.floor(size * 100) / 100;
  162. return `${size} ${units[idx]}`;
  163. }
  164. function displayMeta(key, value, isRo) {
  165. let li = document.createElement("li");
  166. let type = value?.type || null;
  167. let val = (value?.value !== undefined ? value.value : value);
  168. if (!val && val !== '')
  169. return null;
  170. if (type === 'date')
  171. val = val.toLocaleString();
  172. if (["dimension", "height", "width"].indexOf(key) >= 0)
  173. val += ' px';
  174. if (["fileSize"].indexOf(key) >= 0)
  175. val = serializeFileSize(val);
  176. if (key == 'fNumber')
  177. val = `f/ ${val}`;
  178. let keyTranslate = {
  179. fileSize: "File Size",
  180. photochamberImport: "Photochamber Imported",
  181. lensModel: "Lens Model",
  182. exposureTimeStr: "Exposure Time"
  183. };
  184. let keySpan = document.createElement('span');
  185. let valSpan = document.createElement('div');
  186. li.classList.add("row");
  187. keySpan.className = "metaKey col-xl-12 col-4";
  188. valSpan.className = "metaVal col-xl-12 col-8";
  189. let inputGroup = document.createElement('form');
  190. valSpan.appendChild(inputGroup);
  191. inputGroup.classList.add('input-group');
  192. keySpan.innerText = (keyTranslate[key] || (key[0].toUpperCase()+key.substr(1))) + ':';
  193. let valInput = document.createElement("input");
  194. valInput.classList.add("form-control");
  195. valInput.value = val;
  196. valInput.disabled = isRo;
  197. valInput.addEventListener('keyup', evt => evt.stopPropagation());
  198. valInput.addEventListener('keydown', evt => evt.stopPropagation());
  199. inputGroup.appendChild(valInput);
  200. if (!isRo) {
  201. let bt = document.createElement('button');
  202. bt.className = 'btn btn-outline-secondary';
  203. bt.type = 'button';
  204. bt.innerHTML = '<i class="bi bi-pen"></i>';
  205. inputGroup.appendChild(bt);
  206. bt.addEventListener('click', () => MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed.md5sum, key, valInput.value));
  207. inputGroup.addEventListener('submit', evt => {
  208. evt.preventDefault();
  209. MediaStorage.Instance.setMetaValue(fullPageMediaDisplayed.md5sum, key, valInput.value);
  210. });
  211. }
  212. li.appendChild(keySpan);
  213. li.appendChild(valSpan);
  214. return li;
  215. }
  216. function displayMetas(metaData, isRo) {
  217. let metaList = document.createElement("ul");
  218. metaData.libraryPath = null;
  219. metaData.tags = metaData.fixedTags = null;
  220. if (metaData.exposureTime && metaData.exposureTimeStr)
  221. metaData.exposureTime = null;
  222. if (metaData.date && metaData.dateTime)
  223. metaData.dateTime = null;
  224. if (metaData.height && metaData.width) {
  225. metaData.dimension = { type: "string", value: `${metaData.width.value} x ${metaData.height.value}` }
  226. metaData.height = metaData.width = null;
  227. }
  228. for (let i of [ "date", "dimension", "height", "width", "fileSize" ]) {
  229. let metaItem = displayMeta(i, metaData[i], true);
  230. metaItem && metaList.appendChild(metaItem);
  231. metaData[i] = null;
  232. }
  233. for (let i of Object.keys(metaData).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))) {
  234. let metaItem = displayMeta(i, metaData[i], isRo || !MediaStorage.Instance.allMetaTypes[i]?.canWrite);
  235. metaItem && metaList.appendChild(metaItem);
  236. }
  237. if (!isRo)
  238. for (let i of ['geoCountry']) {
  239. if (!metaData[i]) {
  240. let metaItem = displayMeta(i, "", false);
  241. metaItem && metaList.appendChild(metaItem);
  242. }
  243. }
  244. return metaList;
  245. }
  246. function displayTags(fixedTagList, tagList, writeAccess) {
  247. let tagListUi = document.createElement("ul");
  248. tagListUi.className = "taglist";
  249. let createTagUiItem = (i, roTag) => {
  250. let uiItem = document.createElement("li");
  251. uiItem.className = "badge text-bg-light";
  252. let textItem = document.createElement("span");
  253. textItem.textContent = i;
  254. uiItem.appendChild(textItem);
  255. if (!roTag) {
  256. let btItem = document.createElement("a");
  257. btItem.className = "border-start bi bi-x removeBt";
  258. btItem.href = '#';
  259. btItem.addEventListener('click', e => {
  260. e.preventDefault();
  261. });
  262. uiItem.appendChild(btItem);
  263. }
  264. tagListUi.appendChild(uiItem);
  265. };
  266. for (let i of fixedTagList)
  267. createTagUiItem(i, true);
  268. for (let i of tagList)
  269. createTagUiItem(i, !writeAccess);
  270. return tagListUi;
  271. }
  272. function _displayMediaFullPage(fileName, imgUrl, metaData, downloadLink, writeAccess) {
  273. return new Promise(ok => {
  274. document.getElementById("pch-fullPagePreviewContainer").classList.add("loading");
  275. document.getElementById("pch-fullPageMedia-title").innerText = fileName;
  276. document.getElementById("pch-fullPagePreview").onceLoaded = ok;
  277. document.getElementById("pch-fullPagePreview").src = imgUrl;
  278. document.getElementById("pch-fullPageDetail").innerText = "";
  279. document.getElementById("pch-fullPageDetail").appendChild(displayMetas(Object.assign({}, metaData || {}), !writeAccess));
  280. document.getElementById("pch-fullPageDetail").appendChild(displayTags(metaData?.fixedTags || [], metaData?.tags || [], writeAccess));
  281. if (downloadLink) {
  282. document.getElementById("pch-fullPageDetail-dlButton").classList.remove("hidden");
  283. document.getElementById("pch-fullPageDetail-dlButton").dataset["link"] = downloadLink;
  284. } else {
  285. document.getElementById("pch-fullPageDetail-dlButton").classList.add("hidden");
  286. }
  287. });
  288. }
  289. function LoadPreviousMedia() {
  290. let media = MediaStorage.Instance.previousMedia(fullPageMediaDisplayed);
  291. if (media)
  292. window.displayMediaFullPage(media);
  293. }
  294. function LoadNextMedia() {
  295. let media = MediaStorage.Instance.nextMedia(fullPageMediaDisplayed);
  296. if (media)
  297. window.displayMediaFullPage(media);
  298. }
  299. function CloseFullpageMedia() {
  300. document.getElementById("pch-fullPageMedia").classList.add("hidden");
  301. fullPageMediaDisplayed = false;
  302. history.pushState({}, '', '#');
  303. }
  304. window.displayMediaFullPage = function(mediaItem) {
  305. document.getElementById("pch-fullPageMedia").classList.remove("hidden");
  306. fullPageMediaDisplayed = mediaItem;
  307. if (!mediaItem)
  308. return _displayMediaFullPage("Error", null, {}, [], null, false);
  309. let containerSize = document.getElementById("pch-fullPageMedia").getBoundingClientRect();
  310. let requestSize = mediaItem.resize(containerSize.width, containerSize.height);
  311. document.getElementById("pch-fullPagePreview").parentNode.style.maxWidth = "100%";
  312. document.getElementById("pch-fullPagePreview").parentNode.style.maxHeight = "100%";
  313. let meta = {
  314. ...mediaItem.meta,
  315. date: { type: 'date', value: mediaItem.date } || undefined,
  316. filename: mediaItem.filename ? { type: 'string', value: mediaItem.fileName } : undefined,
  317. fixedTags: mediaItem.fixedTags,
  318. tags: mediaItem.tags
  319. };
  320. if (document.location.hash != `#${mediaItem.md5sum}`)
  321. history.pushState({}, '', `#${mediaItem.md5sum}`);
  322. return _displayMediaFullPage(mediaItem.fileName, `${mediaItem.thumbnail}?w=${requestSize.width}&h=${requestSize.height}&q=6`, meta, mediaItem.original, mediaItem.writeAccess);
  323. }
  324. document.getElementById("pch-fullPageMedia-closeBt")
  325. .addEventListener("click", () => CloseFullpageMedia());
  326. document.getElementById("pch-fullPagePreview").addEventListener("load", () => {
  327. document.getElementById("pch-fullPagePreviewContainer").classList.remove("loading");
  328. let domItem = document.getElementById("pch-fullPagePreview");
  329. domItem.onceLoaded && domItem.onceLoaded();
  330. domItem.onceLoaded = null;
  331. });
  332. document.getElementById('pch-fullPageDetail-dlButton').addEventListener("click", (evt) => {
  333. if (!evt.target?.dataset?.link)
  334. return;
  335. let link = document.createElement('a');
  336. link.target='_blank';
  337. link.setAttribute("download", "");
  338. link.href = evt.target.dataset.link;
  339. link.click();
  340. });
  341. document.addEventListener("keyup", evt => {
  342. lastKeyboardEvent = evt;
  343. });
  344. document.addEventListener("keydown", evt => {
  345. lastKeyboardEvent = evt;
  346. if (!fullPageMediaDisplayed)
  347. return;
  348. if (evt.keyCode === 37 || evt.keyCode === 38)
  349. LoadPreviousMedia();
  350. else if (evt.keyCode === 39 || evt.keyCode === 40)
  351. LoadNextMedia();
  352. else if (evt.keyCode === 27)
  353. CloseFullpageMedia();
  354. else {
  355. console.log("Unregistered key event", evt.key, evt.keyCode);
  356. return;
  357. }
  358. evt.preventDefault();
  359. });
  360. });