Compare commits

...

3 Commits

Author SHA1 Message Date
Stefan Forstenlechner 80f11b2539 Navigation is allowed to all folders
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 4s Details
Build and publish docker image snapshot / build-and-publish (push) Has been cancelled Details
regardless of if images are contained in a folder

Some cleanup is not done yet. See TODO
2024-08-20 23:02:23 +02:00
Stefan Forstenlechner 25d17c5cde 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.
2024-08-20 22:39:42 +02:00
Stefan Forstenlechner b4d5b6a5bd minor changes in docker-snapshot.yaml 2024-08-19 21:41:55 +02:00
15 changed files with 230 additions and 293 deletions

View File

@ -7,9 +7,10 @@ jobs:
runs-on: ubuntu-latest
if: gitea.ref == 'refs/heads/master'
steps:
- uses: https://github.com/actions/checkout@v4
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
@ -17,7 +18,7 @@ jobs:
username: stefan
password: ${{ secrets.ACTION_TOKEN }}
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
uses: docker/build-push-action@v5
with:
context: .
push: true

View File

@ -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",

View File

@ -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",

View File

@ -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 (
<img
src={folder.imagePreview.src}
alt={folder.name}
loading="lazy"
style={{
objectFit: "cover",
width: "100%",
height: "100%",
clipPath: "url(#folderPath)",
}}
/>
);
};
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 (
<>
<ImageList cols={columns} gap={8} style={{ overflowY: "initial" }}>
{folders.map((folder) => (
<ImageListItem key={folder.fullPath}>
{/* Link and image styling taken from https://github.com/mui/material-ui/issues/22597 */}
<Link
to={folder.fullPath}
style={{ display: "block", height: "100%" }}
>
<img
src={folder.imagePreviewSrc}
alt={folder.name}
loading="lazy"
<ColumnsPhotoAlbum
photos={foldersAsImages}
render={{
image: (props, context) => (
<PreviewFolder folder={context.photo.folderPreview} />
),
link: (props, context) => {
return (
<Link
to={context.photo.href!}
{...props}
style={{
objectFit: "cover",
width: "100%",
height: "100%",
clipPath: "url(#folderPath)",
// TODO: how to fix `react/prop-types`
// eslint-disable-next-line react/prop-types
...props.style,
height: context.height,
}}
/>
<ImageListItemBar
title={folder.name}
actionIcon={
<Chip
label={folder.numberOfFiles}
size="small"
sx={{
color: "white",
backgroundColor: "rgba(255, 255, 255, 0.24);",
marginRight: 1,
}}
/>
}
/>
</Link>
</ImageListItem>
))}
</ImageList>
{/* External svg does not seem to work (anymore?) */}
{/* see for example https://codepen.io/imohkay/pen/GJpxXY */}
);
},
extras: (props, context) => (
<div
style={{
width: "100%",
color: "#fff",
backgroundColor: "rgba(0, 0, 0, 0.5)",
position: "absolute",
top: "50%",
transform: "translateY(-50%)",
padding: "10px 0px 10px 0px",
pointerEvents: "none",
}}
>
<Typography noWrap style={{ textAlign: "center" }}>
{context.photo.folderPreview.name}
</Typography>
</div>
),
}}
/>
{/* External SVG is not supported, except in Firefox. See https://caniuse.com/css-clip-path */}
{/* See for example https://codepen.io/imohkay/pen/GJpxXY */}
<svg
height={0}
width={0}

View File

@ -1,33 +1,32 @@
import React, { useState } from "react";
import { ImageWithThumbnail } from "./models";
import { Lightbox } from "yet-another-react-lightbox";
import "yet-another-react-lightbox/styles.css";
import Fullscreen from "yet-another-react-lightbox/plugins/fullscreen";
import Slideshow from "yet-another-react-lightbox/plugins/slideshow";
import Thumbnails from "yet-another-react-lightbox/plugins/thumbnails";
import "yet-another-react-lightbox/plugins/thumbnails.css";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import { ImageList, ImageListItem } from "@mui/material";
import { useColumns } from "../util/responsive";
import "yet-another-react-lightbox/plugins/thumbnails.css";
import { MasonryPhotoAlbum } from "react-photo-album";
import "react-photo-album/masonry.css";
export const ImageGallery = ({ images }: { images: ImageWithThumbnail[] }) => {
const [index, setIndex] = useState(-1);
const columns = useColumns();
return (
<>
<ImageList variant="masonry" cols={columns} gap={8}>
{images.map((item, index) => (
<ImageListItem key={item.thumbnail}>
<img
src={item.thumbnail}
loading="lazy"
onClick={() => setIndex(index)}
/>
</ImageListItem>
))}
</ImageList>
<MasonryPhotoAlbum
photos={images}
componentsProps={{
image: {
loading: "lazy",
},
}}
onClick={({ index }) => setIndex(index)}
/>
<Lightbox
slides={images}
open={index >= 0}

View File

@ -4,7 +4,7 @@ import FolderOpenIcon from "@mui/icons-material/FolderOpen";
import PhotoOutlined from "@mui/icons-material/PhotoOutlined";
import { SimpleTreeView, TreeItem } from "@mui/x-tree-view";
import { useLocation, useNavigate } from "react-router-dom";
import React, { useMemo, useState } from "react";
import React, { useEffect, useState } from "react";
import { Folders } from "./models";
import Toolbar from "@mui/material/Toolbar";
import { Chip, useTheme } from "@mui/material";
@ -15,7 +15,7 @@ import Typography from "@mui/material/Typography";
function generateTreeViewChildren(
folders: Folders[],
navigateAndToggleExpand: (_path: string, _navigationAllowed: boolean) => void,
navigateAndToggleExpand: (_path: string) => void,
) {
return (
<>
@ -28,16 +28,13 @@ function generateTreeViewChildren(
{f.name} <Chip label={f.numberOfFiles} size="small" />
</>
);
const containsImages = f.numberOfFiles > 0;
if (f.children.length === 0) {
return (
<TreeItem
key={f.fullPath}
itemId={f.fullPath}
label={label}
onClick={() =>
navigateAndToggleExpand(f.fullPath, containsImages)
}
onClick={() => navigateAndToggleExpand(f.fullPath)}
/>
);
}
@ -46,7 +43,7 @@ function generateTreeViewChildren(
key={f.fullPath}
itemId={f.fullPath}
label={label}
onClick={() => navigateAndToggleExpand(f.fullPath, containsImages)}
onClick={() => navigateAndToggleExpand(f.fullPath)}
>
{generateTreeViewChildren(f.children, navigateAndToggleExpand)}
</TreeItem>
@ -56,29 +53,32 @@ function generateTreeViewChildren(
);
}
const calcFolderWithItem = (
cur: Folders,
calculated: Set<string>,
): Set<string> => {
if (cur.numberOfFiles > 0 || cur.children.length == 0) {
calculated.add(cur.fullPath);
}
cur.children.forEach((a) => calcFolderWithItem(a, calculated));
return calculated;
};
const GenerateTreeView = ({ root }: { root: Folders }) => {
const location = useLocation();
const navigate = useNavigate();
const folderFullPathContainingPhotos = useMemo(
() => calcFolderWithItem(root, new Set()),
[root],
);
const [expandedItems, setExpandedItems] = useState<string[]>(
getDefaultExpanded(location.pathname),
);
const [selectedItem, setSelectedItem] = useState<string>(location.pathname);
// TODO: clean this effect up. See also `getDefaultExpanded`
useEffect(() => {
let curPathname = location.pathname.startsWith("/")
? location.pathname.slice(1)
: location.pathname;
while (curPathname.endsWith("/")) {
curPathname = curPathname.slice(0, -1);
}
const parentPathname = curPathname.substring(
0,
curPathname.lastIndexOf("/"),
);
if (!expandedItems.includes(parentPathname)) {
setExpandedItems([parentPathname, ...expandedItems]);
}
setSelectedItem(curPathname);
}, [location]);
const toggleExpanded = (path: string) => {
if (expandedItems.includes(path)) {
setExpandedItems(expandedItems.filter((p) => p !== path));
@ -87,16 +87,11 @@ const GenerateTreeView = ({ root }: { root: Folders }) => {
}
};
const navigateAndToggleExpand = (
path: string,
navigationAllowed: boolean,
) => {
if (!navigationAllowed || location.pathname === path) {
toggleExpanded(path);
return;
}
const navigateAndToggleExpand = (path: string) => {
toggleExpanded(path);
navigate(path);
if (location.pathname !== path) {
navigate(path);
}
};
return (
@ -109,7 +104,7 @@ const GenerateTreeView = ({ root }: { root: Folders }) => {
expandedItems={expandedItems}
selectedItems={selectedItem}
onSelectedItemsChange={(event, itemId) => {
if (itemId != null && folderFullPathContainingPhotos.has(itemId)) {
if (itemId != null) {
setSelectedItem(itemId);
}
}}

View File

@ -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;
}

View File

@ -109,21 +109,21 @@ function ImageGalleryLayout() {
<>
{foldersPreview.length > 0 && (
<>
<Divider>
<Divider style={{ marginBottom: "10px" }}>
<Chip label="Folders" size="small" />
</Divider>
<FolderGallery folders={foldersPreview} />
</>
)}
{images.length > 0 && foldersPreview.length > 0 && (
<Divider>
<Divider style={{ marginBottom: "10px" }}>
<Chip label="Images" size="small" />
</Divider>
)}
{images.length > 0 && <ImageGallery images={images} />}
{images.length == 0 && foldersPreview.length == 0 && (
<p>
No images available. You may want to add images in your root
No images available. You may want to add images to this
directory.
</p>
)}

View File

@ -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);
}

View File

@ -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 = <TValue>(
value: TValue | void | null | undefined,
): value is TValue => {
return value !== null && value !== undefined;
};

View File

@ -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<Folders> => {
const dirEnts = fs.readdirSync(path.posix.join(publicPath, dirPath), {
@ -29,7 +30,9 @@ export const walk = async (dirPath: string): Promise<Folders> => {
};
};
const getFirstImageInFolder = (dirPath: string): string | undefined => {
const getFirstImageInFolder = async (
dirPath: string,
): Promise<Image | void> => {
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<FolderPreview>({
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.` });

View File

@ -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 = <TValue>(
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<Image>({
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<Image | void> =>
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 (

View File

@ -19,8 +19,7 @@ export interface Folders {
export interface FolderPreview {
name: string;
fullPath: string;
numberOfFiles: number;
imagePreviewSrc: string | undefined;
imagePreview: Image;
}
export const a = <T>(v: T): T => {

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB