From 25d17c5cde2cbe02c8e81034b579a1efd6d88762 Mon Sep 17 00:00:00 2001 From: Stefan Forstenlechner Date: Tue, 20 Aug 2024 22:39:42 +0200 Subject: [PATCH] Revert back to react-photo-album and improve FolderGallery MUI ImageList loads images one after another, which leads to loading all images even with loading=lazy. Maybe it depends on the order in which images are loaded, but this issue never arose with react-photo-album FolderGallery always displays folder icons in the same way and simply positions the image to cover the space available. This circumvents the issue of different aspect ratios of images. --- picture-gallery-client/package-lock.json | 147 ++++-------------- picture-gallery-client/package.json | 1 + .../src/ImageGallery/FolderGallery.tsx | 119 ++++++++------ .../src/ImageGallery/ImageGallery.tsx | 29 ++-- .../src/ImageGallery/models.ts | 7 +- .../src/ImageGalleryLayout.tsx | 4 +- picture-gallery-client/src/util/responsive.ts | 11 -- .../src/controller/common.ts | 23 ++- .../src/controller/directories.ts | 47 +++--- .../src/controller/images.ts | 54 +++---- picture-gallery-server/src/models.ts | 3 +- picture-gallery-server/src/thumbnails.ts | 10 +- public/myimages/singleImage/portrait.jpg | Bin 0 -> 14573 bytes 13 files changed, 198 insertions(+), 257 deletions(-) delete mode 100644 picture-gallery-client/src/util/responsive.ts create mode 100644 public/myimages/singleImage/portrait.jpg diff --git a/picture-gallery-client/package-lock.json b/picture-gallery-client/package-lock.json index cd714f9..765e8e6 100644 --- a/picture-gallery-client/package-lock.json +++ b/picture-gallery-client/package-lock.json @@ -18,6 +18,9 @@ "@types/node": "^22.2.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "8.0.1", + "@typescript-eslint/parser": "8.0.1", + "@vitejs/plugin-react": "^4.3.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", "eslint": "8.57.0", @@ -30,24 +33,22 @@ "eslint-plugin-react-hooks": "^4.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-photo-album": "^3.0.1", "react-router-dom": "^6.26.0", "typescript": "^5.5.4", + "vite": "^5.4.0", + "vite-plugin-eslint": "^1.8.1", + "vite-tsconfig-paths": "^5.0.1", "web-vitals": "^4.2.3", "yet-another-react-lightbox": "^3.21.3" }, "devDependencies": { "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", - "@typescript-eslint/eslint-plugin": "8.0.1", - "@typescript-eslint/parser": "8.0.1", - "@vitejs/plugin-react": "^4.3.1", "@vitest/coverage-v8": "^2.0.5", "jsdom": "^24.1.1", "prettier": "^3.3.3", "ts-node": "^10.9.2", - "vite": "^5.4.0", - "vite-plugin-eslint": "^1.8.1", - "vite-tsconfig-paths": "^5.0.1", "vitest": "^2.0.5" } }, @@ -55,7 +56,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -80,7 +80,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -89,7 +88,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -118,14 +116,12 @@ "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -148,7 +144,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.25.2", "@babel/helper-validator-option": "^7.24.8", @@ -164,7 +159,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -185,7 +179,6 @@ "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", - "dev": true, "dependencies": { "@babel/helper-module-imports": "^7.24.7", "@babel/helper-simple-access": "^7.24.7", @@ -203,7 +196,6 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -212,7 +204,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, "dependencies": { "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7" @@ -241,7 +232,6 @@ "version": "7.24.8", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -250,7 +240,6 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", - "dev": true, "dependencies": { "@babel/template": "^7.25.0", "@babel/types": "^7.25.0" @@ -291,7 +280,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -306,7 +294,6 @@ "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.24.7" }, @@ -539,7 +526,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -555,7 +541,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -571,7 +556,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -587,7 +571,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -603,7 +586,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -619,7 +601,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -635,7 +616,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -651,7 +631,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -667,7 +646,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -683,7 +661,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -699,7 +676,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -715,7 +691,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -731,7 +706,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -747,7 +721,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -763,7 +736,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -779,7 +751,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -795,7 +766,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -811,7 +781,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -827,7 +796,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -843,7 +811,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -859,7 +826,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -875,7 +841,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -891,7 +856,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1592,7 +1556,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", - "dev": true, "dependencies": { "estree-walker": "^2.0.1", "picomatch": "^2.2.2" @@ -1608,7 +1571,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -1621,7 +1583,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -1634,7 +1595,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1647,7 +1607,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -1660,7 +1619,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1673,7 +1631,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1686,7 +1643,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1699,7 +1655,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1712,7 +1667,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1725,7 +1679,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1738,7 +1691,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1751,7 +1703,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1764,7 +1715,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -1777,7 +1727,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1790,7 +1739,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1803,7 +1751,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -1980,7 +1927,6 @@ "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -1993,7 +1939,6 @@ "version": "7.6.8", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, "dependencies": { "@babel/types": "^7.0.0" } @@ -2002,7 +1947,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -2012,7 +1956,6 @@ "version": "7.20.6", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, "dependencies": { "@babel/types": "^7.20.7" } @@ -2021,7 +1964,6 @@ "version": "8.56.11", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.11.tgz", "integrity": "sha512-sVBpJMf7UPo/wGecYOpk2aQya2VUGeHhe38WG7/mN5FufNSubf5VT9Uh9Uyp8/eLJpu1/tuhJ/qTo4mhSB4V4Q==", - "devOptional": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -2030,14 +1972,12 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "devOptional": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "devOptional": true + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -2091,7 +2031,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", - "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.0.1", @@ -2124,7 +2063,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", - "dev": true, "dependencies": { "@typescript-eslint/scope-manager": "8.0.1", "@typescript-eslint/types": "8.0.1", @@ -2152,7 +2090,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", - "dev": true, "dependencies": { "@typescript-eslint/types": "8.0.1", "@typescript-eslint/visitor-keys": "8.0.1" @@ -2169,7 +2106,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", - "dev": true, "dependencies": { "@typescript-eslint/typescript-estree": "8.0.1", "@typescript-eslint/utils": "8.0.1", @@ -2193,7 +2129,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", - "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -2206,7 +2141,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", - "dev": true, "dependencies": { "@typescript-eslint/types": "8.0.1", "@typescript-eslint/visitor-keys": "8.0.1", @@ -2234,7 +2168,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -2243,7 +2176,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2258,7 +2190,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.0.1", @@ -2280,7 +2211,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", - "dev": true, "dependencies": { "@typescript-eslint/types": "8.0.1", "eslint-visitor-keys": "^3.4.3" @@ -2302,7 +2232,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", - "dev": true, "dependencies": { "@babel/core": "^7.24.5", "@babel/plugin-transform-react-jsx-self": "^7.24.5", @@ -2581,7 +2510,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -2798,7 +2726,6 @@ "version": "4.23.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2865,7 +2792,6 @@ "version": "1.0.30001651", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -3283,7 +3209,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -3337,8 +3262,7 @@ "node_modules/electron-to-chromium": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", - "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", - "dev": true + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -3550,7 +3474,6 @@ "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -3588,7 +3511,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "dev": true, "engines": { "node": ">=6" } @@ -4099,8 +4021,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/esutils": { "version": "2.0.3", @@ -4327,7 +4248,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -4444,7 +4364,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4463,8 +4382,7 @@ "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" }, "node_modules/gopd": { "version": "1.0.1", @@ -5258,7 +5176,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -5364,7 +5281,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -5502,7 +5418,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -5524,8 +5439,7 @@ "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -5867,7 +5781,6 @@ "version": "8.4.41", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6049,11 +5962,27 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, + "node_modules/react-photo-album": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-photo-album/-/react-photo-album-3.0.1.tgz", + "integrity": "sha512-yk3FJAuQn8UPZufbdYURa1wy+3tVVNaflyMOQ0mC8dlAWbDhlLDWJ0oGKSbbZkHMX7L3SGRIyWieix++AlD+TQ==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6222,7 +6151,6 @@ "version": "4.20.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", - "dev": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -6363,7 +6291,6 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, "bin": { "semver": "bin/semver.js" }, @@ -6455,7 +6382,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -6472,7 +6398,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -6899,7 +6824,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, "engines": { "node": ">=16" }, @@ -6954,7 +6878,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", - "dev": true, "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -7143,7 +7066,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -7197,7 +7119,6 @@ "version": "5.4.0", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", - "dev": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.40", @@ -7278,7 +7199,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", - "dev": true, "dependencies": { "@rollup/pluginutils": "^4.2.1", "@types/eslint": "^8.4.5", @@ -7293,7 +7213,6 @@ "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", - "dev": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -7308,7 +7227,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", - "dev": true, "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -7798,8 +7716,7 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { "version": "1.10.2", diff --git a/picture-gallery-client/package.json b/picture-gallery-client/package.json index 9a5a644..8c60ae9 100644 --- a/picture-gallery-client/package.json +++ b/picture-gallery-client/package.json @@ -42,6 +42,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-photo-album": "^3.0.1", "react-router-dom": "^6.26.0", "typescript": "^5.5.4", "vite": "^5.4.0", diff --git a/picture-gallery-client/src/ImageGallery/FolderGallery.tsx b/picture-gallery-client/src/ImageGallery/FolderGallery.tsx index d176f40..03cef0e 100644 --- a/picture-gallery-client/src/ImageGallery/FolderGallery.tsx +++ b/picture-gallery-client/src/ImageGallery/FolderGallery.tsx @@ -1,58 +1,87 @@ import React from "react"; -import { FolderPreview } from "./models"; -import { useColumns } from "../util/responsive"; -import { - Chip, - ImageList, - ImageListItem, - ImageListItemBar, -} from "@mui/material"; +import { FolderPreview, ImageWithThumbnail } from "./models"; import { Link } from "react-router-dom"; +import Typography from "@mui/material/Typography"; + +import { ColumnsPhotoAlbum } from "react-photo-album"; +import "react-photo-album/columns.css"; + +export interface PhotoWithFolder extends ImageWithThumbnail { + folderPreview: FolderPreview; +} + +const PreviewFolder = ({ folder }: { folder: FolderPreview }) => { + return ( + {folder.name} + ); +}; export const FolderGallery = ({ folders }: { folders: FolderPreview[] }) => { - const columns = useColumns(); + // hard + const foldersAsImages: PhotoWithFolder[] = folders.map((f) => ({ + ...f.imagePreview, + href: f.fullPath, + folderPreview: f, + // hardcode width and height, so that the aspect ratio looks good for a folder icon + // the image is put in place with `object-fit: "cover"` + width: 290, + height: 230, + })); return ( <> - - {folders.map((folder) => ( - - {/* Link and image styling taken from https://github.com/mui/material-ui/issues/22597 */} - - {folder.name} ( + + ), + link: (props, context) => { + return ( + - - } - /> - - - ))} - - {/* External svg does not seem to work (anymore?) */} - {/* see for example https://codepen.io/imohkay/pen/GJpxXY */} + ); + }, + extras: (props, context) => ( +
+ + {context.photo.folderPreview.name} + +
+ ), + }} + /> + {/* External SVG is not supported, except in Firefox. See https://caniuse.com/css-clip-path */} + {/* See for example https://codepen.io/imohkay/pen/GJpxXY */} { const [index, setIndex] = useState(-1); - const columns = useColumns(); return ( <> - - {images.map((item, index) => ( - - setIndex(index)} - /> - - ))} - + setIndex(index)} + /> = 0} diff --git a/picture-gallery-client/src/ImageGallery/models.ts b/picture-gallery-client/src/ImageGallery/models.ts index 5921a09..3a0bc3e 100644 --- a/picture-gallery-client/src/ImageGallery/models.ts +++ b/picture-gallery-client/src/ImageGallery/models.ts @@ -1,4 +1,4 @@ -import { Slide } from "yet-another-react-lightbox"; +import { Photo } from "react-photo-album"; export interface Folders { name: string; @@ -10,10 +10,9 @@ export interface Folders { export interface FolderPreview { name: string; fullPath: string; - numberOfFiles: number; - imagePreviewSrc: string | undefined; + imagePreview: ImageWithThumbnail; } -export interface ImageWithThumbnail extends Slide { +export interface ImageWithThumbnail extends Photo { thumbnail: string; } diff --git a/picture-gallery-client/src/ImageGalleryLayout.tsx b/picture-gallery-client/src/ImageGalleryLayout.tsx index e195afd..2beb074 100644 --- a/picture-gallery-client/src/ImageGalleryLayout.tsx +++ b/picture-gallery-client/src/ImageGalleryLayout.tsx @@ -109,14 +109,14 @@ function ImageGalleryLayout() { <> {foldersPreview.length > 0 && ( <> - + )} {images.length > 0 && foldersPreview.length > 0 && ( - + )} diff --git a/picture-gallery-client/src/util/responsive.ts b/picture-gallery-client/src/util/responsive.ts deleted file mode 100644 index ca7dda4..0000000 --- a/picture-gallery-client/src/util/responsive.ts +++ /dev/null @@ -1,11 +0,0 @@ -import useMediaQuery from "@mui/material/useMediaQuery"; - -const breakpoints = Object.freeze([1200, 600, 300, 0]); - -export function useColumns(): number { - const values = [5, 4, 3, 2]; - const index = breakpoints - .map((b) => useMediaQuery(`(min-width:${b}px)`)) - .findIndex((a) => a); - return Math.max(values[Math.max(index, 0)], 1); -} diff --git a/picture-gallery-server/src/controller/common.ts b/picture-gallery-server/src/controller/common.ts index a470d39..ce3a29a 100644 --- a/picture-gallery-server/src/controller/common.ts +++ b/picture-gallery-server/src/controller/common.ts @@ -2,6 +2,7 @@ import express from "express"; import path from "path"; import fs, { Dirent } from "fs"; import { thumbnailPath, thumbnailPublicPath } from "../paths"; +import { createThumbnailAsyncForImage } from "../thumbnails"; export const getRequestedPath = (req: express.Request): string => req.params[1] === undefined || req.params[1] === "/" ? "" : req.params[1]; @@ -20,10 +21,20 @@ export const getSrc = (requestedPath: string, f: Dirent): string => path.posix.join("/staticImages", requestedPath, f.name); export const getThumbnail = ( - thumbnailExists: boolean, - requestedPath: string, + filePath: string, f: Dirent, -): string => - thumbnailExists - ? path.posix.join("/staticImages", thumbnailPath, requestedPath, f.name) - : getSrc(requestedPath, f); + thumbnailExists: boolean, +): string => { + if (thumbnailExists) { + return path.posix.join("/staticImages", thumbnailPath, filePath, f.name); + } + + createThumbnailAsyncForImage(path.posix.join(filePath, f.name)); + return getSrc(filePath, f); +}; + +export const notEmpty = ( + value: TValue | void | null | undefined, +): value is TValue => { + return value !== null && value !== undefined; +}; diff --git a/picture-gallery-server/src/controller/directories.ts b/picture-gallery-server/src/controller/directories.ts index 72e8d2b..e3f111b 100644 --- a/picture-gallery-server/src/controller/directories.ts +++ b/picture-gallery-server/src/controller/directories.ts @@ -1,11 +1,12 @@ import fs from "fs"; import * as path from "path"; import express from "express"; -import { a, FolderPreview, Folders } from "../models"; +import { a, FolderPreview, Folders, Image } from "../models"; import { publicPath, thumbnailPath, thumbnailPublicPath } from "../paths"; import { securityValidation } from "./securityChecks"; -import { getRequestedPath, getThumbnail } from "./common"; +import { getRequestedPath, notEmpty } from "./common"; import { consoleLogger } from "../logging"; +import { getImage } from "./images"; export const walk = async (dirPath: string): Promise => { const dirEnts = fs.readdirSync(path.posix.join(publicPath, dirPath), { @@ -29,7 +30,9 @@ export const walk = async (dirPath: string): Promise => { }; }; -const getFirstImageInFolder = (dirPath: string): string | undefined => { +const getFirstImageInFolder = async ( + dirPath: string, +): Promise => { const dirs = [dirPath]; while (dirs.length > 0) { const curPath = dirs.shift(); @@ -38,30 +41,22 @@ const getFirstImageInFolder = (dirPath: string): string | undefined => { }); for (let i = 0; i < dirContent.length; i += 1) { const content = dirContent[i]; - const filePath = path.posix.join(curPath, content.name); if (content.isFile()) { - return getThumbnail( - fs.existsSync(path.posix.join(thumbnailPublicPath, filePath)), - curPath, - content, + const thumbnailExists = fs.existsSync( + path.posix.join(thumbnailPublicPath, curPath, content.name), ); + return getImage(content, curPath, thumbnailExists); } if (content.isDirectory()) { - dirs.push(filePath); + const nextDirPath = path.posix.join(curPath, content.name); + dirs.push(nextDirPath); } } } return undefined; }; -const getNumberOfFiles = (dirPath: string): number => - fs - .readdirSync(path.posix.join(publicPath, dirPath), { - withFileTypes: true, - }) - .filter((d) => d.isFile()).length; - -export const getFolderPreview = ( +export const getFolderPreview = async ( req: express.Request, res: express.Response, ) => { @@ -77,17 +72,23 @@ export const getFolderPreview = ( .filter((d) => d.isDirectory()) .filter((d) => !d.name.includes(thumbnailPath.substring(1))); - res.json( - dirents.map((dir) => { - const fullPath = path.posix.join(requestedPath, dir.name); + const folderPreviewsToLoad = dirents.map(async (dir) => { + const fullPath = path.posix.join(requestedPath, dir.name); + const imageForPreview = await getFirstImageInFolder(fullPath); + if (notEmpty(imageForPreview)) { return a({ name: dir.name, fullPath, - numberOfFiles: getNumberOfFiles(fullPath), - imagePreviewSrc: getFirstImageInFolder(fullPath), + imagePreview: imageForPreview, }); - }), + } + return undefined; + }); + + const folderPreviews = (await Promise.all(folderPreviewsToLoad)).filter( + notEmpty, ); + res.json(folderPreviews); } catch (e) { consoleLogger.warn(`Error when trying to access ${req.path}: ${e}`); res.status(400).json({ message: `Path ${req.path} not accessible.` }); diff --git a/picture-gallery-server/src/controller/images.ts b/picture-gallery-server/src/controller/images.ts index 614ef5a..7beea43 100644 --- a/picture-gallery-server/src/controller/images.ts +++ b/picture-gallery-server/src/controller/images.ts @@ -5,37 +5,49 @@ import path from "path"; import natsort from "natsort"; import { publicPath } from "../paths"; import { a, Folder, Image } from "../models"; -import { createThumbnailAsyncForImage } from "../thumbnails"; import { consoleLogger } from "../logging"; import { securityValidation } from "./securityChecks"; import { getRequestedPath, getSrc, getThumbnail, + notEmpty, readThumbnails, } from "./common"; -const notEmpty = ( - value: TValue | void | null | undefined, -): value is TValue => { - return value !== null && value !== undefined; -}; - const toImage = ( metadata: sharp.Metadata, - thumbnailExists: boolean, - requestedPath: string, + filePath: string, f: Dirent, + thumbnailExists: boolean, ): Image => { const widthAndHeightSwap = metadata.orientation > 4; // see https://exiftool.org/TagNames/EXIF.html return a({ - src: getSrc(requestedPath, f), - thumbnail: getThumbnail(thumbnailExists, requestedPath, f), + src: getSrc(filePath, f), + thumbnail: getThumbnail(filePath, f, thumbnailExists), width: widthAndHeightSwap ? metadata.height : metadata.width, height: widthAndHeightSwap ? metadata.width : metadata.height, }); }; +export const getImage = async ( + f: Dirent, + filePath: string, + thumbnailExists: boolean, +): Promise => + sharp(path.posix.join(publicPath, filePath, f.name)) + .metadata() + .then((metadata) => toImage(metadata, filePath, f, thumbnailExists)) + .catch((err) => { + consoleLogger.error( + `Reading metadata from ${path.posix.join( + publicPath, + filePath, + f.name, + )} produced the following error: ${err.message}`, + ); + }); + const getImagesToBeLoaded = ( dirents: Dirent[], thumbnails: string[], @@ -46,25 +58,9 @@ const getImagesToBeLoaded = ( // sorts by name in a natural way // could be made configurable for sorting in other ways (e.g. date of creation) .sort((file1, file2) => natsort()(file1.name, file2.name)) - .map((f) => { + .map(async (f) => { const thumbnailExists: boolean = thumbnails.includes(f.name); - if (!thumbnailExists) { - createThumbnailAsyncForImage(path.posix.join(requestedPath, f.name)); - } - return sharp(path.posix.join(publicPath, requestedPath, f.name)) - .metadata() - .then((metadata) => - toImage(metadata, thumbnailExists, requestedPath, f), - ) - .catch((err) => { - consoleLogger.error( - `Reading metadata from ${path.posix.join( - publicPath, - requestedPath, - f.name, - )} produced the following error: ${err.message}`, - ); - }); + return getImage(f, requestedPath, thumbnailExists); }); export const getImages = async ( diff --git a/picture-gallery-server/src/models.ts b/picture-gallery-server/src/models.ts index 802aee5..3baf9f1 100644 --- a/picture-gallery-server/src/models.ts +++ b/picture-gallery-server/src/models.ts @@ -19,8 +19,7 @@ export interface Folders { export interface FolderPreview { name: string; fullPath: string; - numberOfFiles: number; - imagePreviewSrc: string | undefined; + imagePreview: Image; } export const a = (v: T): T => { diff --git a/picture-gallery-server/src/thumbnails.ts b/picture-gallery-server/src/thumbnails.ts index ceb210e..1784e35 100644 --- a/picture-gallery-server/src/thumbnails.ts +++ b/picture-gallery-server/src/thumbnails.ts @@ -14,11 +14,11 @@ export const createThumbnailAsyncForImage = (image: string) => { .then((info) => { const width = Math.max( Math.min(info.width, minimumPixelForThumbnail), - Math.round((info.width * percentage) / 100) + Math.round((info.width * percentage) / 100), ); const height = Math.max( Math.min(info.height, minimumPixelForThumbnail), - Math.round((info.height * percentage) / 100) + Math.round((info.height * percentage) / 100), ); fs.mkdir( @@ -29,12 +29,12 @@ export const createThumbnailAsyncForImage = (image: string) => { .withMetadata() .resize(info.width > info.height ? { width } : { height }) .toFile(`${path.posix.join(thumbnailPublicPath, image)}`); - } + }, ); }) .catch((err) => { consoleLogger.error( - `Thumbnail creation of ${publicImagePath} produced the following error: ${err.message}` + `Thumbnail creation of ${publicImagePath} produced the following error: ${err.message}`, ); }); }; @@ -50,7 +50,7 @@ export const initThumbnailsAsync = (dirPath: string) => { recursive: true, }); const thumbnails = fs.readdirSync( - path.posix.join(thumbnailPublicPath, dirPath) + path.posix.join(thumbnailPublicPath, dirPath), ); dirEnts diff --git a/public/myimages/singleImage/portrait.jpg b/public/myimages/singleImage/portrait.jpg new file mode 100644 index 0000000000000000000000000000000000000000..72da474b11ba9a9b42a4f77c965fb34d550555eb GIT binary patch literal 14573 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
  • |Mar55__>W@TdNMh~6g_Eip1NldRIv%iG=y);xt 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 literal 0 HcmV?d00001