photo.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870
  1. /*
  2. Photo.js
  3. Author: tobychui
  4. This is a complete rewrite of the legacy Photo module for ArozOS
  5. */
  6. // Number of photos to load per page (infinite scroll batch size)
  7. const PAGE_SIZE = 40; //large enough to fill the whole page on load, but small enough to keep initial load fast and responsive
  8. let photoList = [];
  9. let prePhoto = "";
  10. let nextPhoto = "";
  11. let currentModel = "";
  12. let currentPhotoAllIndex = -1; // index of current photo in allImages (full server list)
  13. let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  14. // Check if image should use compression (only JPG/PNG)
  15. function shouldUseCompression(filepath, filesize) {
  16. const ext = filepath.split('.').pop().toLowerCase();
  17. const isJpgOrPng = (ext === 'jpg' || ext === 'jpeg' || ext === 'png');
  18. const COMPRESSION_THRESHOLD = 5 * 1024 * 1024; // 5MB
  19. return isJpgOrPng && filesize && filesize > COMPRESSION_THRESHOLD;
  20. }
  21. // Get viewable image URL (handles RAW files)
  22. function getViewableImageUrl(filepath, callback) {
  23. // Both RAW and regular images now use backend rendering
  24. const imageUrl = "../media?file=" + encodeURIComponent(filepath);
  25. callback(imageUrl, true, false, isRawImage(filepath) ? 'backend_raw' : 'direct');
  26. }
  27. function getImageWidth(){
  28. // Use the actual viewbox container width so the sidebar and scrollbar are
  29. // already subtracted — this prevents gaps when the window is resized.
  30. const container = document.getElementById('viewboxContainer');
  31. const containerWidth = container ? container.clientWidth : (window.innerWidth - 210);
  32. let boxCount;
  33. if (containerWidth < 400) {
  34. boxCount = 2;
  35. } else if (containerWidth < 600) {
  36. boxCount = 3;
  37. } else if (containerWidth < 900) {
  38. boxCount = 4;
  39. } else if (containerWidth < 1100) {
  40. boxCount = 5;
  41. } else if (containerWidth < 1400) {
  42. boxCount = 6;
  43. } else {
  44. boxCount = 8;
  45. }
  46. return Math.floor(containerWidth / boxCount);
  47. }
  48. function updateImageSizes(){
  49. let newImageWidth = getImageWidth();
  50. console.log(newImageWidth, $("#viewbox").width());
  51. //Updates all the size of the images
  52. $(".imagecard").css({
  53. width: newImageWidth,
  54. height: newImageWidth
  55. });
  56. }
  57. function extractFolderName(folderpath){
  58. return folderpath.split("/").pop();
  59. }
  60. function parseExifValue(value) {
  61. if (typeof value === 'string' && value.includes('/')) {
  62. let parts = value.split('/');
  63. if (parts.length === 2) {
  64. let num = parseFloat(parts[0]);
  65. let den = parseFloat(parts[1]);
  66. if (den !== 0) {
  67. return num / den;
  68. }
  69. }
  70. }
  71. return parseFloat(value) || value;
  72. }
  73. function formatShutterSpeed(value) {
  74. let num = parseExifValue(value);
  75. if (num < 1) {
  76. return "1/" + Math.round(1 / num);
  77. } else {
  78. return num ;
  79. }
  80. }
  81. function photoListObject() {
  82. return {
  83. // data
  84. pathWildcard: "user:/Photo/*",
  85. currentPath: "user:/Photo",
  86. renderSize: 200,
  87. vroots: [],
  88. allImages: [], // full list from server
  89. images: [], // currently displayed slice
  90. folders: [],
  91. sortOrder: 'smart',
  92. restored: false,
  93. hasMoreImages: false,
  94. isLoadingMore: false, // guard: blocks new batch until DOM has updated
  95. sidebarOpen: !isMobile, // start hidden on mobile, visible on desktop
  96. // init
  97. init() {
  98. this.getFolderInfo();
  99. this.getRootInfo();
  100. this.renderSize = getImageWidth();
  101. updateImageSizes();
  102. this.restored = false;
  103. this.$nextTick(() => { this.setupInfiniteScroll(); });
  104. const MOBILE_BP = 768;
  105. let _prevMobile = window.innerWidth <= MOBILE_BP;
  106. let _resizeTimer;
  107. window.addEventListener('resize', () => {
  108. clearTimeout(_resizeTimer);
  109. _resizeTimer = setTimeout(() => {
  110. // Recalculate tile sizes
  111. this.renderSize = getImageWidth();
  112. updateImageSizes();
  113. // Auto-manage sidebar visibility on breakpoint crossing
  114. const nowMobile = window.innerWidth <= MOBILE_BP;
  115. if (nowMobile && !_prevMobile) {
  116. // Desktop → mobile: hide sidebar so it doesn't overlay content
  117. this.sidebarOpen = false;
  118. } else if (!nowMobile && _prevMobile) {
  119. // Mobile → desktop: sidebar is back in normal flow, keep state clean
  120. this.sidebarOpen = false;
  121. }
  122. _prevMobile = nowMobile;
  123. }, 80);
  124. });
  125. },
  126. updateRenderingPath(newPath, callback = null){
  127. this.currentPath = JSON.parse(JSON.stringify(newPath));
  128. this.pathWildcard = newPath + '/*';
  129. this.restored = false;
  130. if (isMobile) this.sidebarOpen = false;
  131. this.getFolderInfo(callback);
  132. },
  133. // Returns path segments for the sidebar breadcrumb tree
  134. getPathSegments() {
  135. const parts = this.currentPath.split('/');
  136. let segments = [];
  137. let accumulated = '';
  138. for (let i = 0; i < parts.length; i++) {
  139. accumulated = i === 0 ? parts[0] : accumulated + '/' + parts[i];
  140. segments.push({ name: parts[i], path: accumulated, depth: i, isDiskRoot: i === 0 });
  141. }
  142. return segments;
  143. },
  144. getFolderInfo(callback = null) {
  145. fetch(ao_root + "system/ajgi/interface?script=Photo/backend/listFolder.js", {
  146. method: 'POST',
  147. cache: 'no-cache',
  148. headers: {
  149. 'Content-Type': 'application/json'
  150. },
  151. body: JSON.stringify({
  152. "folder": this.pathWildcard,
  153. "sort": this.sortOrder
  154. })
  155. }).then(resp => {
  156. resp.json().then(data => {
  157. console.log(data);
  158. this.folders = data[0];
  159. this.allImages = data[1];
  160. this.images = this.allImages.slice(0, PAGE_SIZE);
  161. this.hasMoreImages = this.allImages.length > PAGE_SIZE;
  162. this.isLoadingMore = false;
  163. if (this.allImages.length == 0){
  164. $("#noimg").show();
  165. }else{
  166. $("#noimg").hide();
  167. }
  168. console.log(this.folders);
  169. if (!this.restored) { restoreFromHash(); this.restored = true; }
  170. if (callback) callback();
  171. });
  172. });
  173. },
  174. getRootInfo() {
  175. fetch(ao_root + "system/ajgi/interface?script=Photo/backend/listRoots.js", {
  176. method: 'POST',
  177. cache: 'no-cache',
  178. headers: {
  179. 'Content-Type': 'application/json'
  180. },
  181. body: JSON.stringify({})
  182. }).then(resp => {
  183. resp.json().then(data => {
  184. this.vroots = data;
  185. this.$nextTick(() => {
  186. $('.ui.dropdown').dropdown();
  187. });
  188. });
  189. })
  190. },
  191. changeSort(newSort) {
  192. this.sortOrder = newSort;
  193. this.getFolderInfo();
  194. },
  195. // Load the next PAGE_SIZE images into the displayed list
  196. loadMoreImages() {
  197. if (this.isLoadingMore) return;
  198. const current = this.images.length;
  199. if (current >= this.allImages.length) return;
  200. this.isLoadingMore = true;
  201. const next = this.allImages.slice(current, current + PAGE_SIZE);
  202. this.images = this.images.concat(next);
  203. this.hasMoreImages = this.images.length < this.allImages.length;
  204. // Release the guard only after Alpine has re-rendered and scrollHeight has grown
  205. this.$nextTick(() => { this.isLoadingMore = false; });
  206. },
  207. // Attach a scroll listener to the viewbox container for infinite scroll
  208. setupInfiniteScroll() {
  209. const container = document.getElementById('viewboxContainer');
  210. if (!container) return;
  211. container.addEventListener('scroll', () => {
  212. const { scrollTop, scrollHeight, clientHeight } = container;
  213. // Trigger when within 300px of the bottom
  214. if (scrollTop + clientHeight >= scrollHeight - 300) {
  215. this.loadMoreImages();
  216. }
  217. });
  218. }
  219. }
  220. }
  221. function renderImageList(object){
  222. var fd = $(object).attr("filedata");
  223. fd = JSON.parse(decodeURIComponent(fd));
  224. console.log(fd);
  225. }
  226. function ShowModal(){
  227. $('#photo-viewer').show();
  228. }
  229. function closeViewer(){
  230. $('#photo-viewer').hide();
  231. if (!ao_module_virtualDesktop){
  232. // Only update hash if not under WebDesktop mode
  233. // to prevent iframe refresh
  234. window.location.hash = '';
  235. }
  236. ao_module_setWindowTitle("Photo");
  237. setTimeout(function(){
  238. $("#fullImage").attr("src","img/loading.png");
  239. $("#compressedImage").attr("src","").hide().removeClass('hidden');
  240. $("#bg-image").attr("src","");
  241. $("#info-filename").text("");
  242. $("#info-filepath").text("");
  243. $("#info-dimensions").text("Loading...");
  244. // Reset EXIF data display
  245. $('#basic-info-section').hide();
  246. $('#shooting-params-section').hide();
  247. $('#tone-analysis-section').hide();
  248. $('#device-info-section').hide();
  249. $('#shooting-mode-section').hide();
  250. $('#technical-params-section').hide();
  251. $('#no-exif-message').hide();
  252. $('.ui.divider').hide();
  253. // Clear histogram canvas
  254. const canvas = document.getElementById('histogram-canvas');
  255. if (canvas) {
  256. const ctx = canvas.getContext('2d');
  257. ctx.clearRect(0, 0, canvas.width, canvas.height);
  258. }
  259. }, 300);
  260. }
  261. let compressedImageLoaded = false;
  262. let fullsizeImageLoaded = false;
  263. function showImage(object){
  264. // Reset zoom level when switching photos
  265. if (typeof resetZoom === 'function') {
  266. resetZoom();
  267. }
  268. if (!$(object).hasClass("imagecard")){
  269. // Not an image card, do nothing
  270. return;
  271. }
  272. // Reset loading flags
  273. compressedImageLoaded = false;
  274. fullsizeImageLoaded = false;
  275. var fd = JSON.parse(decodeURIComponent($(object).attr("filedata")));
  276. $("#info-dimensions").text("Calculating...");
  277. // Check if we should use compression (only for JPG/PNG > 5MB)
  278. const useCompression = shouldUseCompression(fd.filepath, fd.filesize);
  279. // Set thumbnail as placeholder for full image
  280. const thumbnailUrl = $(object).find('img').attr('src');
  281. $("#fullImage").attr("src", thumbnailUrl);
  282. $("#fullImage").hide();
  283. $("#compressedImage").show();
  284. $("#compressedImage").attr("src", thumbnailUrl);
  285. $("#bg-image").attr("src", thumbnailUrl);
  286. // Get image URL (backend handles RAW files automatically)
  287. getViewableImageUrl(fd.filepath, (imageUrl, isSupported, isBlob, method) => {
  288. $("#loading-progress").show();
  289. const compressedImg = document.getElementById('compressedImage');
  290. const fullImg = document.getElementById('fullImage');
  291. const bgImg = document.getElementById('bg-image');
  292. $("#loading-progress").html(`<i class="loading spinner icon"></i> Loading`);
  293. if (useCompression) {
  294. // Use compressed version for large JPG/PNG files
  295. console.log('Large JPG/PNG detected (' + (fd.filesize / 1024 / 1024).toFixed(2) + 'MB), loading compressed version first');
  296. fetch(ao_root + "system/ajgi/interface?script=Photo/backend/getCompressedImg.js", {
  297. method: 'POST',
  298. cache: 'no-cache',
  299. headers: {
  300. 'Content-Type': 'application/json'
  301. },
  302. body: JSON.stringify({
  303. "filepath": fd.filepath
  304. })
  305. }).then(resp => {
  306. resp.text().then(dataURL => {
  307. $("#loading-progress").html(`<i class="loading spinner icon"></i> Optimizing Resolution`);
  308. compressedImageLoaded = true;
  309. // Only show compressed image if full-size hasn't loaded yet
  310. if (!fullsizeImageLoaded) {
  311. compressedImg.src = dataURL;
  312. compressedImg.style.display = 'block';
  313. bgImg.src = dataURL;
  314. } else {
  315. console.log('Full-size image already loaded, skipping compressed image display');
  316. }
  317. });
  318. }).catch(error => {
  319. console.error('Failed to load compressed image:', error);
  320. // Fall back to full size image
  321. fullImg.src = imageUrl;
  322. bgImg.src = imageUrl;
  323. });
  324. // Start loading full-size image in background
  325. loadFullSizeImageInBackground(imageUrl, fd);
  326. } else {
  327. $("#compressedImage").hide();
  328. $("#fullImage").show();
  329. $("#loading-progress").hide();
  330. // Use full image URL directly for RAW, WEBP, or small JPG/PNG files
  331. if (method === 'backend_raw') {
  332. console.log('RAW file: Rendered by backend');
  333. }
  334. fullImg.src = imageUrl;
  335. bgImg.src = imageUrl;
  336. }
  337. // Update image dimensions and generate histogram when full image loads
  338. $("#fullImage").off("load").on('load', function() {
  339. fullsizeImageLoaded = true;
  340. let width = this.naturalWidth;
  341. let height = this.naturalHeight;
  342. $("#info-dimensions").text(width + ' × ' + height + "px");
  343. // Hide the compressed image once full image is loaded
  344. $("#compressedImage").hide();
  345. $("#fullImage").show();
  346. $("#loading-progress").hide();
  347. const canvas = document.getElementById('histogram-canvas');
  348. if (canvas) {
  349. generateHistogram(this, canvas);
  350. }
  351. });
  352. $("#info-filename").text(fd.filename);
  353. $("#info-filepath").text(fd.filepath);
  354. var nextCard = $(object).next();
  355. var prevCard = $(object).prev();
  356. if (nextCard.length > 0){
  357. nextPhoto = nextCard[0];
  358. }else{
  359. nextPhoto = null;
  360. }
  361. if (prevCard.length > 0){
  362. prePhoto = prevCard[0];
  363. }else{
  364. prePhoto = null;
  365. }
  366. // Track position in the full allImages list for index-based navigation
  367. const _appEl = document.querySelector('[x-data*="photoListObject"]');
  368. if (_appEl && _appEl._x_dataStack) {
  369. const _app = _appEl._x_dataStack[0];
  370. currentPhotoAllIndex = _app.allImages.findIndex(img => img.filepath === fd.filepath);
  371. // Proactively load next batch when within PAGE_SIZE of the end of rendered images
  372. if (currentPhotoAllIndex >= 0 && _app.hasMoreImages &&
  373. currentPhotoAllIndex >= _app.images.length - PAGE_SIZE) {
  374. _app.loadMoreImages();
  375. }
  376. }
  377. // Update navigation buttons state
  378. if (typeof updateNavigationButtons === 'function') {
  379. updateNavigationButtons();
  380. }
  381. ao_module_setWindowTitle("Photo - " + fd.filename);
  382. if (!ao_module_virtualDesktop){
  383. window.location.hash = encodeURIComponent(JSON.stringify({filename: fd.filename, filepath: fd.filepath}));
  384. }
  385. // Check for EXIF data
  386. fetch(ao_root + "system/ajgi/interface?script=Photo/backend/getExif.js", {
  387. method: 'POST',
  388. cache: 'no-cache',
  389. headers: {
  390. 'Content-Type': 'application/json'
  391. },
  392. body: JSON.stringify({
  393. "filepath": fd.filepath
  394. })
  395. }).then(resp => {
  396. resp.json().then(data => {
  397. formatExifData(data, fd);
  398. })
  399. }).catch(error => {
  400. console.error('Failed to fetch EXIF data:', error);
  401. formatExifData({}, fd); // Call with empty EXIF to show tone analysis
  402. });
  403. });
  404. }
  405. // Function to load full-size image in background with progress tracking
  406. function loadFullSizeImageInBackground(fullSizeUrl, fileData) {
  407. console.log('Starting background download of full-size image...');
  408. const fullImage = document.getElementById('fullImage');
  409. fullImage.src = fullSizeUrl;
  410. }
  411. $(document).on("keydown", function(e){
  412. if (e.keyCode == 27){ // Escape
  413. if ($('#photo-viewer').is(':visible')) {
  414. closeViewer();
  415. }
  416. } else if (e.keyCode == 37){
  417. //Left
  418. if (typeof showPreviousImage === 'function') {
  419. showPreviousImage();
  420. } else if (prePhoto != null){
  421. showImage(prePhoto);
  422. }
  423. }else if (e.keyCode == 39){
  424. //Right
  425. if (typeof showNextImage === 'function') {
  426. showNextImage();
  427. } else if (nextPhoto != null){
  428. showImage(nextPhoto);
  429. }
  430. }
  431. })
  432. function generateToneAnalysis(imageElement) {
  433. analysis_tone_types(imageElement, function(result) {
  434. if (result) {
  435. // Update tone type based on brightness, contrast, shadow and highlight ratios
  436. let toneType = get_tone_type(result.brightness, result.contrast, result.shadowRatio, result.highlightRatio);
  437. $('.tone-type-value').text(toneType);
  438. $('.brightness-value').text(result.brightness);
  439. $('.contrast-value').text(result.contrast);
  440. $('.shadow-ratio-value').text(result.shadowRatio);
  441. $('.highlight-ratio-value').text(result.highlightRatio);
  442. } else {
  443. $('.tone-type-value').text("N/A");
  444. $('.brightness-value').text("N/A");
  445. $('.contrast-value').text("N/A");
  446. $('.shadow-ratio-value').text("N/A");
  447. $('.highlight-ratio-value').text("N/A");
  448. }
  449. });
  450. }
  451. function formatExifData(exif, fileData) {
  452. // Hide all sections initially
  453. $('#basic-info-section').hide();
  454. $('#shooting-params-section').hide();
  455. $('#tone-analysis-section').hide();
  456. $('#device-info-section').hide();
  457. $('#shooting-mode-section').hide();
  458. $('#technical-params-section').hide();
  459. $('#no-exif-message').hide();
  460. // Hide all dividers
  461. $('.ui.divider').hide();
  462. if (!exif || Object.keys(exif).length === 0) {
  463. $('#no-exif-message').show();
  464. //Generate histogram and tone analysis only
  465. generateHistogram(document.getElementById('fullImage'), document.getElementById('histogram-canvas'));
  466. generateToneAnalysis(document.getElementById('fullImage'));
  467. $('#tone-analysis-section').show();
  468. return;
  469. }
  470. let sectionsShown = 0;
  471. // Section 1: Basic Information
  472. let basicInfoShown = false;
  473. if (fileData.filename) {
  474. let ext = fileData.filename.split('.').pop().toUpperCase();
  475. $('#format-value').text(ext);
  476. $('#format-row').show();
  477. basicInfoShown = true;
  478. } else {
  479. $('#format-row').hide();
  480. }
  481. if (exif.PixelXDimension && exif.PixelYDimension) {
  482. $('#dimensions-value').text(`${exif.PixelXDimension} × ${exif.PixelYDimension}`);
  483. $('#dimensions-row').show();
  484. let pixels = (exif.PixelXDimension * exif.PixelYDimension / 1000000).toFixed(1);
  485. $('#pixels-value').text(`${pixels} MP`);
  486. $('#pixels-row').show();
  487. basicInfoShown = true;
  488. } else {
  489. $('#dimensions-row').hide();
  490. $('#pixels-row').hide();
  491. }
  492. if (exif.ColorSpace !== undefined) {
  493. exif.ColorSpace = JSON.parse(exif.ColorSpace);
  494. let colorSpace = exif.ColorSpace === 1 ? "sRGB" : exif.ColorSpace === 65535 ? "Uncalibrated" : "Unknown";
  495. $('#color-space-value').text(colorSpace);
  496. $('#color-space-row').show();
  497. basicInfoShown = true;
  498. } else {
  499. $('#color-space-row').hide();
  500. }
  501. if (exif.DateTimeOriginal) {
  502. exif.DateTimeOriginal = JSON.parse(exif.DateTimeOriginal);
  503. $('#shooting-time-value').text(exif.DateTimeOriginal.replace(/:/g, '/').replace(' ', ' '));
  504. $('#shooting-time-row').show();
  505. basicInfoShown = true;
  506. } else {
  507. $('#shooting-time-row').hide();
  508. }
  509. if (exif.Software) {
  510. exif.Software = JSON.parse(exif.Software);
  511. $('#software-value').text(exif.Software);
  512. $('#software-row').show();
  513. basicInfoShown = true;
  514. } else {
  515. $('#software-row').hide();
  516. }
  517. if (basicInfoShown) {
  518. $('#basic-info-section').show();
  519. sectionsShown++;
  520. if (sectionsShown > 1) $('#basic-info-divider').show();
  521. }
  522. // Section 2: Shooting Parameters
  523. let shootingParamsShown = false;
  524. if (exif.FocalLength) {
  525. $('#focal-length-value').text(JSON.parse(exif.FocalLength));
  526. $('#focal-length-row').show();
  527. shootingParamsShown = true;
  528. } else {
  529. $('#focal-length-row').hide();
  530. }
  531. if (exif.FNumber) {
  532. exif.FNumber = JSON.parse(exif.FNumber);
  533. let aperture = parseExifValue(exif.FNumber);
  534. let formattedAperture = aperture % 1 === 0 ? aperture.toString() : aperture.toFixed(1);
  535. $('#aperture-value').text('f/' + formattedAperture);
  536. $('#aperture-row').show();
  537. shootingParamsShown = true;
  538. } else {
  539. $('#aperture-row').hide();
  540. }
  541. if (exif.ExposureTime) {
  542. let exposureTime = JSON.parse(exif.ExposureTime);
  543. let formattedExposure = formatShutterSpeed(exposureTime);
  544. $('#shutter-speed-value').text(formattedExposure + 's');
  545. $('#shutter-speed-row').show();
  546. shootingParamsShown = true;
  547. } else {
  548. $('#shutter-speed-row').hide();
  549. }
  550. if (exif.ISOSpeedRatings) {
  551. $('#iso-value').text(exif.ISOSpeedRatings);
  552. $('#iso-row').show();
  553. shootingParamsShown = true;
  554. } else {
  555. $('#iso-row').hide();
  556. }
  557. if (exif.ExposureBiasValue) {
  558. $('#ev-value').text(JSON.parse(exif.ExposureBiasValue));
  559. $('#ev-row').show();
  560. shootingParamsShown = true;
  561. } else {
  562. $('#ev-row').hide();
  563. }
  564. if (shootingParamsShown) {
  565. $('#shooting-params-section').show();
  566. sectionsShown++;
  567. if (sectionsShown > 1) $('#shooting-params-divider').show();
  568. }
  569. // Section 3: Tone Analysis
  570. $('#tone-analysis-section').show();
  571. sectionsShown++;
  572. if (sectionsShown > 1) $('#tone-analysis-divider').show();
  573. generateToneAnalysis(document.getElementById('fullImage'));
  574. // Section 4: Device Information
  575. let deviceInfoShown = false;
  576. if (exif.Make && exif.Model) {
  577. exif.Make = JSON.parse(exif.Make);
  578. exif.Model = JSON.parse(exif.Model);
  579. $('#camera-value').text(`${exif.Make} ${exif.Model}`);
  580. $('#camera-row').show();
  581. deviceInfoShown = true;
  582. } else if (exif.Model) {
  583. exif.Model = JSON.parse(exif.Model);
  584. $('#camera-value').text(exif.Model);
  585. $('#camera-row').show();
  586. deviceInfoShown = true;
  587. } else {
  588. $('#camera-row').hide();
  589. }
  590. if (exif.LensModel) {
  591. exif.LensModel = JSON.parse(exif.LensModel);
  592. $('#lens-value').text(exif.LensModel);
  593. $('#lens-row').show();
  594. deviceInfoShown = true;
  595. } else {
  596. $('#lens-row').hide();
  597. }
  598. if (exif.FocalLength) {
  599. exif.FocalLength = JSON.parse(exif.FocalLength);
  600. $('#focal-length-device-value').text(`${exif.FocalLength}mm`);
  601. $('#focal-length-device-row').show();
  602. deviceInfoShown = true;
  603. } else {
  604. $('#focal-length-device-row').hide();
  605. }
  606. if (exif.MaxApertureValue) {
  607. exif.MaxApertureValue = JSON.parse(exif.MaxApertureValue);
  608. $('#max-aperture-value').text(exif.MaxApertureValue);
  609. $('#max-aperture-row').show();
  610. deviceInfoShown = true;
  611. } else {
  612. $('#max-aperture-row').hide();
  613. }
  614. if (deviceInfoShown) {
  615. $('#device-info-section').show();
  616. sectionsShown++;
  617. if (sectionsShown > 1) $('#device-info-divider').show();
  618. }
  619. // Section 5: Shooting Mode
  620. let shootingModeShown = false;
  621. if (exif.ExposureProgram !== undefined) {
  622. let programs = ["Not defined", "Manual", "Normal program", "Aperture priority", "Shutter priority", "Creative program", "Action program", "Portrait mode", "Landscape mode"];
  623. let program = programs[exif.ExposureProgram] || "Unknown";
  624. $('#exposure-program-value').text(program);
  625. $('#exposure-program-row').show();
  626. shootingModeShown = true;
  627. } else {
  628. $('#exposure-program-row').hide();
  629. }
  630. if (exif.ExposureMode !== undefined) {
  631. let modes = ["Auto exposure", "Manual exposure", "Auto bracket"];
  632. let mode = modes[exif.ExposureMode] || "Unknown";
  633. $('#exposure-mode-value').text(mode);
  634. $('#exposure-mode-row').show();
  635. shootingModeShown = true;
  636. } else {
  637. $('#exposure-mode-row').hide();
  638. }
  639. if (exif.MeteringMode !== undefined) {
  640. let metering = ["Unknown", "Average", "Center-weighted average", "Spot", "Multi-spot", "Pattern", "Partial"];
  641. let meter = metering[exif.MeteringMode] || "Unknown";
  642. $('#metering-mode-value').text(meter);
  643. $('#metering-mode-row').show();
  644. shootingModeShown = true;
  645. } else {
  646. $('#metering-mode-row').hide();
  647. }
  648. if (exif.WhiteBalance !== undefined) {
  649. let wb = exif.WhiteBalance === 0 ? "Auto" : "Manual";
  650. $('#white-balance-value').text(wb);
  651. $('#white-balance-row').show();
  652. shootingModeShown = true;
  653. } else {
  654. $('#white-balance-row').hide();
  655. }
  656. if (exif.Flash !== undefined) {
  657. let flash = (exif.Flash & 1) ? "On" : "Off";
  658. $('#flash-value').text(flash);
  659. $('#flash-row').show();
  660. shootingModeShown = true;
  661. } else {
  662. $('#flash-row').hide();
  663. }
  664. if (exif.SceneCaptureType !== undefined) {
  665. let scenes = ["Standard", "Landscape", "Portrait", "Night scene"];
  666. let scene = scenes[exif.SceneCaptureType] || "Unknown";
  667. $('#scene-capture-value').text(scene);
  668. $('#scene-capture-row').show();
  669. shootingModeShown = true;
  670. } else {
  671. $('#scene-capture-row').hide();
  672. }
  673. if (shootingModeShown) {
  674. $('#shooting-mode-section').show();
  675. sectionsShown++;
  676. if (sectionsShown > 1) $('#shooting-mode-divider').show();
  677. }
  678. // Section 6: Technical Parameters
  679. let technicalParamsShown = false;
  680. if (exif.ShutterSpeedValue) {
  681. exif.ShutterSpeedValue = JSON.parse(exif.ShutterSpeedValue);
  682. let apexValue = parseExifValue(exif.ShutterSpeedValue);
  683. let shutterSpeedSeconds = Math.pow(2, -apexValue);
  684. let shutterValue = formatShutterSpeed(shutterSpeedSeconds);
  685. $('#shutter-speed-tech-value').text(shutterValue);
  686. $('#shutter-speed-tech-row').show();
  687. technicalParamsShown = true;
  688. } else {
  689. $('#shutter-speed-tech-row').hide();
  690. }
  691. if (exif.ApertureValue) {
  692. exif.ApertureValue = JSON.parse(exif.ApertureValue);
  693. let apexValue = parseExifValue(exif.ApertureValue);
  694. let apertureValue = Math.pow(2, apexValue / 2);
  695. $('#aperture-value-value').text(apertureValue.toFixed(1) + ' EV');
  696. $('#aperture-value-row').show();
  697. technicalParamsShown = true;
  698. } else {
  699. $('#aperture-value-row').hide();
  700. }
  701. if (exif.FocalPlaneXResolution && exif.FocalPlaneYResolution) {
  702. exif.FocalPlaneXResolution = JSON.parse(exif.FocalPlaneXResolution);
  703. exif.FocalPlaneYResolution = JSON.parse(exif.FocalPlaneYResolution);
  704. let xRes = parseExifValue(exif.FocalPlaneXResolution);
  705. let yRes = parseExifValue(exif.FocalPlaneYResolution);
  706. $('#focal-plane-res-value').text(Math.round(xRes) + ' × ' + Math.round(yRes));
  707. $('#focal-plane-res-row').show();
  708. technicalParamsShown = true;
  709. } else {
  710. $('#focal-plane-res-row').hide();
  711. }
  712. if (technicalParamsShown) {
  713. $('#technical-params-section').show();
  714. sectionsShown++;
  715. if (sectionsShown > 1) $('#technical-params-divider').show();
  716. }
  717. }
  718. function restoreFromHash() {
  719. if (window.location.hash) {
  720. let hashData = decodeURIComponent(window.location.hash.substring(1));
  721. try {
  722. let data = JSON.parse(hashData);
  723. // Find the element with matching filepath
  724. let elements = document.querySelectorAll('[filedata]');
  725. for (let el of elements) {
  726. let fdStr = el.getAttribute('filedata');
  727. if (fdStr) {
  728. let fd = JSON.parse(decodeURIComponent(fdStr));
  729. if (fd.filepath === data.filepath) {
  730. showImage(el);
  731. ShowModal();
  732. break;
  733. }
  734. }
  735. }
  736. } catch (e) {
  737. console.error('Invalid hash data', e);
  738. }
  739. }
  740. }
  741. // Modify the window onload event to ensure folder and thumbnails are loaded first
  742. window.addEventListener('load', () => {
  743. setTimeout(function(){
  744. if (window.location.hash) {
  745. const hashData = decodeURIComponent(window.location.hash.substring(1));
  746. try {
  747. const data = JSON.parse(hashData);
  748. let filename = data.filename;
  749. let filepath = data.filepath;
  750. let dir = filepath.split("/").slice(0, -1).join("/");
  751. // Access the Alpine data instance
  752. const appElement = document.querySelector('[x-data*="photoListObject"]');
  753. if (appElement) {
  754. const app = appElement._x_dataStack[0];
  755. if (app.currentPath !== dir) {
  756. app.updateRenderingPath(dir, () => {
  757. setTimeout(function(){
  758. console.log("Test")
  759. restoreFromHash();
  760. }, 100);
  761. });
  762. } else {
  763. // Folder is already loaded, try to restore immediately
  764. restoreFromHash();
  765. }
  766. }
  767. } catch (e) {
  768. console.error('Invalid hash data', e);
  769. }
  770. }
  771. }, 100);
  772. });