Browse Source

Merge branch 'immich-app:main' into feat-web-photo-editor

faupau 1 year ago
parent
commit
376a64ac17
76 changed files with 1382 additions and 748 deletions
  1. 2 1
      .github/workflows/cache-cleanup.yml
  2. 2 1
      .github/workflows/cli-release.yml
  3. 1 1
      .github/workflows/docker-cleanup.yml
  4. 2 1
      .github/workflows/docker.yml
  5. 14 10
      .github/workflows/test.yml
  6. 517 75
      cli/package-lock.json
  7. 3 3
      cli/package.json
  8. 1 1
      docker/docker-compose.dev.yml
  9. 58 0
      docs/docs/guides/remote-access.md
  10. 104 16
      docs/package-lock.json
  11. 2 2
      docs/package.json
  12. 9 3
      docs/src/theme/SearchBar/algolia.css
  13. 2 2
      machine-learning/Dockerfile
  14. 1 1
      machine-learning/export/Dockerfile
  15. 0 1
      mobile/analysis_options.yaml
  16. 1 1
      mobile/integration_test/test_utils/general_helper.dart
  17. 3 3
      mobile/lib/constants/immich_colors.dart
  18. 19 9
      mobile/lib/extensions/asyncvalue_extensions.dart
  19. 1 1
      mobile/lib/extensions/build_context_extensions.dart
  20. 33 31
      mobile/lib/main.dart
  21. 4 7
      mobile/lib/modules/activities/views/activities_page.dart
  22. 1 1
      mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
  23. 17 16
      mobile/lib/modules/album/ui/album_viewer_appbar.dart
  24. 8 9
      mobile/lib/modules/album/views/album_options_part.dart
  25. 18 26
      mobile/lib/modules/album/views/album_viewer_page.dart
  26. 3 6
      mobile/lib/modules/album/views/asset_selection_page.dart
  27. 3 7
      mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
  28. 9 8
      mobile/lib/modules/album/views/select_user_for_sharing_page.dart
  29. 34 42
      mobile/lib/modules/archive/views/archive_page.dart
  30. 8 2
      mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart
  31. 4 4
      mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
  32. 3 0
      mobile/lib/modules/backup/views/backup_controller_page.dart
  33. 15 20
      mobile/lib/modules/favorite/views/favorites_page.dart
  34. 3 7
      mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
  35. 3 1
      mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
  36. 8 12
      mobile/lib/modules/home/views/home_page.dart
  37. 12 5
      mobile/lib/modules/login/ui/login_form.dart
  38. 1 1
      mobile/lib/modules/memories/ui/memory_lane.dart
  39. 3 5
      mobile/lib/modules/partner/views/partner_detail_page.dart
  40. 12 8
      mobile/lib/modules/partner/views/partner_page.dart
  41. 3 7
      mobile/lib/modules/search/views/all_motion_videos_page.dart
  42. 3 7
      mobile/lib/modules/search/views/all_people_page.dart
  43. 3 7
      mobile/lib/modules/search/views/all_videos_page.dart
  44. 3 7
      mobile/lib/modules/search/views/curated_location_page.dart
  45. 1 3
      mobile/lib/modules/search/views/person_result_page.dart
  46. 3 7
      mobile/lib/modules/search/views/recently_added_page.dart
  47. 8 9
      mobile/lib/modules/search/views/search_page.dart
  48. 2 3
      mobile/lib/modules/settings/views/settings_page.dart
  49. 16 6
      mobile/lib/modules/shared_link/ui/shared_link_item.dart
  50. 6 1
      mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
  51. 8 6
      mobile/lib/modules/shared_link/views/shared_link_page.dart
  52. 12 20
      mobile/lib/modules/trash/views/trash_page.dart
  53. 1 1
      mobile/lib/shared/ui/immich_loading_indicator.dart
  54. 14 11
      mobile/lib/shared/ui/scaffold_error_body.dart
  55. 16 2
      mobile/lib/shared/views/app_log_detail_page.dart
  56. 52 29
      mobile/lib/shared/views/immich_loading_overlay.dart
  57. 18 18
      mobile/lib/utils/immich_app_theme.dart
  58. 2 2
      mobile/lib/utils/url_helper.dart
  59. 20 8
      renovate.json
  60. 1 0
      server/.eslintrc.js
  61. 3 3
      server/Dockerfile
  62. 90 90
      server/package-lock.json
  63. 3 1
      server/src/domain/metadata/metadata.service.ts
  64. 3 1
      server/src/infra/repositories/system-metadata.repository.ts
  65. 1 0
      web/.eslintrc.cjs
  66. 2 1
      web/Dockerfile
  67. 94 131
      web/package-lock.json
  68. 2 3
      web/package.json
  69. 5 2
      web/src/lib/components/admin-page/restore-dialoge.svelte
  70. 2 1
      web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte
  71. 12 2
      web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte
  72. 2 0
      web/src/lib/components/assets/thumbnail/image-thumbnail.svelte
  73. 2 0
      web/src/lib/components/faces-page/people-card.svelte
  74. 1 1
      web/src/lib/utils/byte-units.ts
  75. 20 6
      web/src/lib/utils/thumbnail-util.ts
  76. 4 2
      web/src/routes/(user)/people/+page.svelte

+ 2 - 1
.github/workflows/cache-cleanup.yml

@@ -1,4 +1,4 @@
-name: Clean up actions cache on PR close
+name: Cache Cleanup
 on:
 on:
   pull_request:
   pull_request:
     types:
     types:
@@ -10,6 +10,7 @@ concurrency:
 
 
 jobs:
 jobs:
   cleanup:
   cleanup:
+    name: Cleanup
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Check out code
       - name: Check out code

+ 2 - 1
.github/workflows/cli-release.yml

@@ -1,9 +1,10 @@
-name: Publish Package to npmjs
+name: CLI Release
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
 
 
 jobs:
 jobs:
   publish:
   publish:
+    name: Publish
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     defaults:
     defaults:
       run:
       run:

+ 1 - 1
.github/workflows/docker-cleanup.yml

@@ -5,7 +5,7 @@
 #
 #
 # This workflow will not trigger runs on forked repos.
 # This workflow will not trigger runs on forked repos.
 
 
-name: Cleanup Old Docker Images
+name: Docker Cleanup
 
 
 on:
 on:
   pull_request:
   pull_request:

+ 2 - 1
.github/workflows/docker.yml

@@ -1,4 +1,4 @@
-name: Build and Push Docker Images
+name: Docker
 
 
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
@@ -18,6 +18,7 @@ permissions:
 
 
 jobs:
 jobs:
   build_and_push:
   build_and_push:
+    name: Build and Push
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     strategy:
     strategy:
       # Prevent a failure in one image from stopping the other builds
       # Prevent a failure in one image from stopping the other builds

+ 14 - 10
.github/workflows/test.yml

@@ -11,7 +11,7 @@ concurrency:
 
 
 jobs:
 jobs:
   e2e-tests:
   e2e-tests:
-    name: Run end-to-end test suites
+    name: Server (e2e)
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
 
 
     steps:
     steps:
@@ -24,7 +24,7 @@ jobs:
         run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
         run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
 
 
   doc-tests:
   doc-tests:
-    name: Run documentation checks
+    name: Docs
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     defaults:
     defaults:
       run:
       run:
@@ -45,8 +45,12 @@ jobs:
         run: npm run check
         run: npm run check
         if: ${{ !cancelled() }}
         if: ${{ !cancelled() }}
 
 
+      - name: Run build
+        run: npm run build
+        if: ${{ !cancelled() }}
+
   server-unit-tests:
   server-unit-tests:
-    name: Run server unit test suites and checks
+    name: Server
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     defaults:
     defaults:
       run:
       run:
@@ -76,7 +80,7 @@ jobs:
         if: ${{ !cancelled() }}
         if: ${{ !cancelled() }}
 
 
   cli-unit-tests:
   cli-unit-tests:
-    name: Run cli test suites
+    name: CLI
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     defaults:
     defaults:
       run:
       run:
@@ -106,7 +110,7 @@ jobs:
         if: ${{ !cancelled() }}
         if: ${{ !cancelled() }}
 
 
   web-unit-tests:
   web-unit-tests:
-    name: Run web unit test suites and checks
+    name: Web
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     defaults:
     defaults:
       run:
       run:
@@ -140,7 +144,7 @@ jobs:
       #   if: ${{ !cancelled() }}
       #   if: ${{ !cancelled() }}
 
 
   mobile-unit-tests:
   mobile-unit-tests:
-    name: Run mobile unit tests
+    name: Mobile
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -154,7 +158,7 @@ jobs:
         run: flutter test -j 1
         run: flutter test -j 1
 
 
   ml-unit-tests:
   ml-unit-tests:
-    name: Run ML unit tests and checks
+    name: Machine Learning
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     defaults:
     defaults:
       run:
       run:
@@ -184,7 +188,7 @@ jobs:
           poetry run pytest --cov app
           poetry run pytest --cov app
 
 
   generated-api-up-to-date:
   generated-api-up-to-date:
-    name: Check generated files are up-to-date
+    name: OpenAPI Clients
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - uses: actions/checkout@v4
       - uses: actions/checkout@v4
@@ -205,11 +209,11 @@ jobs:
           exit 1
           exit 1
 
 
   generated-typeorm-migrations-up-to-date:
   generated-typeorm-migrations-up-to-date:
-    name: Check generated TypeORM migrations are up-to-date
+    name: TypeORM Migrations
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     services:
     services:
       postgres:
       postgres:
-        image: postgres
+        image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c
         env:
         env:
           POSTGRES_PASSWORD: postgres
           POSTGRES_PASSWORD: postgres
           POSTGRES_USER: postgres
           POSTGRES_USER: postgres

+ 517 - 75
cli/package-lock.json

@@ -29,8 +29,8 @@
         "@types/mime-types": "^2.1.1",
         "@types/mime-types": "^2.1.1",
         "@types/mock-fs": "^4.13.1",
         "@types/mock-fs": "^4.13.1",
         "@types/node": "^20.3.1",
         "@types/node": "^20.3.1",
-        "@typescript-eslint/eslint-plugin": "^5.60.1",
-        "@typescript-eslint/parser": "^5.48.1",
+        "@typescript-eslint/eslint-plugin": "^6.0.0",
+        "@typescript-eslint/parser": "^6.0.0",
         "chai": "^4.3.7",
         "chai": "^4.3.7",
         "eslint": "^8.43.0",
         "eslint": "^8.43.0",
         "eslint-config-prettier": "^9.0.0",
         "eslint-config-prettier": "^9.0.0",
@@ -46,7 +46,7 @@
         "ts-jest": "^29.1.0",
         "ts-jest": "^29.1.0",
         "ts-node": "^10.9.1",
         "ts-node": "^10.9.1",
         "tslib": "^2.5.3",
         "tslib": "^2.5.3",
-        "typescript": "^4.9.4"
+        "typescript": "^5.0.0"
       }
       }
     },
     },
     "node_modules/@aashutoshrathi/word-wrap": {
     "node_modules/@aashutoshrathi/word-wrap": {
@@ -1646,32 +1646,90 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
-      "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+      "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@eslint-community/regexpp": "^4.4.0",
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/type-utils": "5.62.0",
-        "@typescript-eslint/utils": "5.62.0",
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/type-utils": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
-        "ignore": "^5.2.0",
-        "natural-compare-lite": "^1.4.0",
-        "semver": "^7.3.7",
-        "tsutils": "^3.21.0"
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "@typescript-eslint/parser": "^5.0.0",
-        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+      "dev": true,
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependenciesMeta": {
       "peerDependenciesMeta": {
         "typescript": {
         "typescript": {
@@ -1679,26 +1737,126 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+      "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
     "node_modules/@typescript-eslint/parser": {
     "node_modules/@typescript-eslint/parser": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
-      "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+      "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/types": "5.62.0",
-        "@typescript-eslint/typescript-estree": "5.62.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+      "dev": true,
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependenciesMeta": {
       "peerDependenciesMeta": {
         "typescript": {
         "typescript": {
@@ -1706,6 +1864,23 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
     "node_modules/@typescript-eslint/scope-manager": {
     "node_modules/@typescript-eslint/scope-manager": {
       "version": "5.62.0",
       "version": "5.62.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
@@ -1724,25 +1899,82 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/type-utils": {
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
-      "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+      "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "5.62.0",
-        "@typescript-eslint/utils": "5.62.0",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
-        "tsutils": "^3.21.0"
+        "ts-api-utils": "^1.0.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "eslint": "*"
+        "eslint": "^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+      "dev": true,
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependenciesMeta": {
       "peerDependenciesMeta": {
         "typescript": {
         "typescript": {
@@ -1750,6 +1982,48 @@
         }
         }
       }
       }
     },
     },
+    "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+      "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+      "dev": true,
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+      "dev": true,
+      "dependencies": {
+        "@typescript-eslint/types": "6.13.1",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^16.0.0 || >=18.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
     "node_modules/@typescript-eslint/types": {
     "node_modules/@typescript-eslint/types": {
       "version": "5.62.0",
       "version": "5.62.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
@@ -4864,12 +5138,6 @@
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "dev": true
       "dev": true
     },
     },
-    "node_modules/natural-compare-lite": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
-      "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
-      "dev": true
-    },
     "node_modules/node-int64": {
     "node_modules/node-int64": {
       "version": "0.4.0",
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -6023,6 +6291,18 @@
         "node": ">=8.0"
         "node": ">=8.0"
       }
       }
     },
     },
+    "node_modules/ts-api-utils": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+      "dev": true,
+      "engines": {
+        "node": ">=16.13.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
     "node_modules/ts-jest": {
     "node_modules/ts-jest": {
       "version": "29.1.1",
       "version": "29.1.1",
       "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
       "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
@@ -6170,16 +6450,16 @@
       }
       }
     },
     },
     "node_modules/typescript": {
     "node_modules/typescript": {
-      "version": "4.9.5",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-      "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+      "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
       "dev": true,
       "dev": true,
       "bin": {
       "bin": {
         "tsc": "bin/tsc",
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
         "tsserver": "bin/tsserver"
       },
       },
       "engines": {
       "engines": {
-        "node": ">=4.2.0"
+        "node": ">=14.17"
       }
       }
     },
     },
     "node_modules/undici-types": {
     "node_modules/undici-types": {
@@ -7752,33 +8032,136 @@
       "dev": true
       "dev": true
     },
     },
     "@typescript-eslint/eslint-plugin": {
     "@typescript-eslint/eslint-plugin": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
-      "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+      "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@eslint-community/regexpp": "^4.4.0",
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/type-utils": "5.62.0",
-        "@typescript-eslint/utils": "5.62.0",
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/type-utils": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
-        "ignore": "^5.2.0",
-        "natural-compare-lite": "^1.4.0",
-        "semver": "^7.3.7",
-        "tsutils": "^3.21.0"
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
+      },
+      "dependencies": {
+        "@typescript-eslint/scope-manager": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+          "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/visitor-keys": "6.13.1"
+          }
+        },
+        "@typescript-eslint/types": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+          "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+          "dev": true
+        },
+        "@typescript-eslint/typescript-estree": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+          "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/visitor-keys": "6.13.1",
+            "debug": "^4.3.4",
+            "globby": "^11.1.0",
+            "is-glob": "^4.0.3",
+            "semver": "^7.5.4",
+            "ts-api-utils": "^1.0.1"
+          }
+        },
+        "@typescript-eslint/utils": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+          "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+          "dev": true,
+          "requires": {
+            "@eslint-community/eslint-utils": "^4.4.0",
+            "@types/json-schema": "^7.0.12",
+            "@types/semver": "^7.5.0",
+            "@typescript-eslint/scope-manager": "6.13.1",
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/typescript-estree": "6.13.1",
+            "semver": "^7.5.4"
+          }
+        },
+        "@typescript-eslint/visitor-keys": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+          "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "eslint-visitor-keys": "^3.4.1"
+          }
+        }
       }
       }
     },
     },
     "@typescript-eslint/parser": {
     "@typescript-eslint/parser": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
-      "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+      "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/types": "5.62.0",
-        "@typescript-eslint/typescript-estree": "5.62.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
+      },
+      "dependencies": {
+        "@typescript-eslint/scope-manager": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+          "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/visitor-keys": "6.13.1"
+          }
+        },
+        "@typescript-eslint/types": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+          "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+          "dev": true
+        },
+        "@typescript-eslint/typescript-estree": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+          "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/visitor-keys": "6.13.1",
+            "debug": "^4.3.4",
+            "globby": "^11.1.0",
+            "is-glob": "^4.0.3",
+            "semver": "^7.5.4",
+            "ts-api-utils": "^1.0.1"
+          }
+        },
+        "@typescript-eslint/visitor-keys": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+          "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "eslint-visitor-keys": "^3.4.1"
+          }
+        }
       }
       }
     },
     },
     "@typescript-eslint/scope-manager": {
     "@typescript-eslint/scope-manager": {
@@ -7792,15 +8175,73 @@
       }
       }
     },
     },
     "@typescript-eslint/type-utils": {
     "@typescript-eslint/type-utils": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
-      "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+      "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/typescript-estree": "5.62.0",
-        "@typescript-eslint/utils": "5.62.0",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
-        "tsutils": "^3.21.0"
+        "ts-api-utils": "^1.0.1"
+      },
+      "dependencies": {
+        "@typescript-eslint/scope-manager": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+          "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/visitor-keys": "6.13.1"
+          }
+        },
+        "@typescript-eslint/types": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+          "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
+          "dev": true
+        },
+        "@typescript-eslint/typescript-estree": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+          "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/visitor-keys": "6.13.1",
+            "debug": "^4.3.4",
+            "globby": "^11.1.0",
+            "is-glob": "^4.0.3",
+            "semver": "^7.5.4",
+            "ts-api-utils": "^1.0.1"
+          }
+        },
+        "@typescript-eslint/utils": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+          "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
+          "dev": true,
+          "requires": {
+            "@eslint-community/eslint-utils": "^4.4.0",
+            "@types/json-schema": "^7.0.12",
+            "@types/semver": "^7.5.0",
+            "@typescript-eslint/scope-manager": "6.13.1",
+            "@typescript-eslint/types": "6.13.1",
+            "@typescript-eslint/typescript-estree": "6.13.1",
+            "semver": "^7.5.4"
+          }
+        },
+        "@typescript-eslint/visitor-keys": {
+          "version": "6.13.1",
+          "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+          "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
+          "dev": true,
+          "requires": {
+            "@typescript-eslint/types": "6.13.1",
+            "eslint-visitor-keys": "^3.4.1"
+          }
+        }
       }
       }
     },
     },
     "@typescript-eslint/types": {
     "@typescript-eslint/types": {
@@ -10035,12 +10476,6 @@
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "dev": true
       "dev": true
     },
     },
-    "natural-compare-lite": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
-      "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
-      "dev": true
-    },
     "node-int64": {
     "node-int64": {
       "version": "0.4.0",
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
       "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -10865,6 +11300,13 @@
         "is-number": "^7.0.0"
         "is-number": "^7.0.0"
       }
       }
     },
     },
+    "ts-api-utils": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+      "dev": true,
+      "requires": {}
+    },
     "ts-jest": {
     "ts-jest": {
       "version": "29.1.1",
       "version": "29.1.1",
       "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
       "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz",
@@ -10947,9 +11389,9 @@
       "dev": true
       "dev": true
     },
     },
     "typescript": {
     "typescript": {
-      "version": "4.9.5",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-      "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
+      "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
       "dev": true
       "dev": true
     },
     },
     "undici-types": {
     "undici-types": {

+ 3 - 3
cli/package.json

@@ -29,8 +29,8 @@
     "@types/mime-types": "^2.1.1",
     "@types/mime-types": "^2.1.1",
     "@types/mock-fs": "^4.13.1",
     "@types/mock-fs": "^4.13.1",
     "@types/node": "^20.3.1",
     "@types/node": "^20.3.1",
-    "@typescript-eslint/eslint-plugin": "^5.60.1",
-    "@typescript-eslint/parser": "^5.48.1",
+    "@typescript-eslint/eslint-plugin": "^6.0.0",
+    "@typescript-eslint/parser": "^6.0.0",
     "chai": "^4.3.7",
     "chai": "^4.3.7",
     "eslint": "^8.43.0",
     "eslint": "^8.43.0",
     "eslint-config-prettier": "^9.0.0",
     "eslint-config-prettier": "^9.0.0",
@@ -46,7 +46,7 @@
     "ts-jest": "^29.1.0",
     "ts-jest": "^29.1.0",
     "ts-node": "^10.9.1",
     "ts-node": "^10.9.1",
     "tslib": "^2.5.3",
     "tslib": "^2.5.3",
-    "typescript": "^4.9.4"
+    "typescript": "^5.0.0"
   },
   },
   "scripts": {
   "scripts": {
     "build": "tsc --project tsconfig.build.json",
     "build": "tsc --project tsconfig.build.json",

+ 1 - 1
docker/docker-compose.dev.yml

@@ -59,7 +59,7 @@ services:
     build:
     build:
       context: ../web
       context: ../web
       dockerfile: Dockerfile
       dockerfile: Dockerfile
-    command: npm run dev --host
+    command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000"
     env_file:
     env_file:
       - .env
       - .env
     ports:
     ports:

+ 58 - 0
docs/docs/guides/remote-access.md

@@ -0,0 +1,58 @@
+# Remote Access
+
+This page gives a few pointers on how to access your Immich instance from outside your LAN.
+
+:::danger
+Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you succeptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks.
+:::
+
+## Option 1: VPN to home network
+
+You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/)
+
+### Pros:
+
+- Simple to set up and very secure.
+- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk.
+- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal.
+
+### Cons:
+
+- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider.
+- VPN software needs to be installed and active on both server-side and client-side.
+- Requires you to open a port on your router to your server.
+
+## Option 2: Tailscale
+
+If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
+
+### Pros
+
+- Minimal configuration needed on server and client sides.
+- You are protected against zero-day vulnerabilities on Immich.
+
+### Cons
+
+- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
+- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
+- Tailscale needs to be installed and running on both server-side and client-side.
+
+## Option 3: Reverse Proxy
+
+A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side.
+
+If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](https://immich.app/docs/administration/reverse-proxy).
+
+You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
+
+A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder.
+
+### Pros
+
+- No additional software needs to be installed client-side
+- If you only need access to the web interface remotely, it is possible to set up access controls that shield you from zero-day vulnerabilities on Immich. [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) has a generous free tier.
+
+### Cons
+
+- Complex configuration
+- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out.

+ 104 - 16
docs/package-lock.json

@@ -15,7 +15,7 @@
         "@mdx-js/react": "^1.6.22",
         "@mdx-js/react": "^1.6.22",
         "autoprefixer": "^10.4.13",
         "autoprefixer": "^10.4.13",
         "classnames": "^2.3.2",
         "classnames": "^2.3.2",
-        "clsx": "^1.2.1",
+        "clsx": "^2.0.0",
         "docusaurus-lunr-search": "^2.3.2",
         "docusaurus-lunr-search": "^2.3.2",
         "docusaurus-preset-openapi": "^0.6.3",
         "docusaurus-preset-openapi": "^0.6.3",
         "postcss": "^8.4.25",
         "postcss": "^8.4.25",
@@ -28,7 +28,7 @@
       "devDependencies": {
       "devDependencies": {
         "@docusaurus/module-type-aliases": "^2.4.1",
         "@docusaurus/module-type-aliases": "^2.4.1",
         "@tsconfig/docusaurus": "^1.0.5",
         "@tsconfig/docusaurus": "^1.0.5",
-        "prettier": "^2.8.8",
+        "prettier": "^3.0.0",
         "typescript": "^5.1.6"
         "typescript": "^5.1.6"
       },
       },
       "engines": {
       "engines": {
@@ -2603,6 +2603,14 @@
         "react-dom": "^16.8.4 || ^17.0.0"
         "react-dom": "^16.8.4 || ^17.0.0"
       }
       }
     },
     },
+    "node_modules/@docusaurus/theme-classic/node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/@docusaurus/theme-common": {
     "node_modules/@docusaurus/theme-common": {
       "version": "2.4.3",
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz",
       "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz",
@@ -2633,6 +2641,14 @@
         "react-dom": "^16.8.4 || ^17.0.0"
         "react-dom": "^16.8.4 || ^17.0.0"
       }
       }
     },
     },
+    "node_modules/@docusaurus/theme-common/node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/@docusaurus/theme-search-algolia": {
     "node_modules/@docusaurus/theme-search-algolia": {
       "version": "2.4.3",
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz",
       "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz",
@@ -2663,6 +2679,14 @@
         "react-dom": "^16.8.4 || ^17.0.0"
         "react-dom": "^16.8.4 || ^17.0.0"
       }
       }
     },
     },
+    "node_modules/@docusaurus/theme-search-algolia/node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/@docusaurus/theme-translations": {
     "node_modules/@docusaurus/theme-translations": {
       "version": "2.4.3",
       "version": "2.4.3",
       "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz",
       "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz",
@@ -4948,9 +4972,9 @@
       }
       }
     },
     },
     "node_modules/clsx": {
     "node_modules/clsx": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
-      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+      "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
       "engines": {
       "engines": {
         "node": ">=6"
         "node": ">=6"
       }
       }
@@ -5995,6 +6019,14 @@
         "react-dom": "^16.8.4 || ^17"
         "react-dom": "^16.8.4 || ^17"
       }
       }
     },
     },
+    "node_modules/docusaurus-lunr-search/node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/docusaurus-plugin-openapi": {
     "node_modules/docusaurus-plugin-openapi": {
       "version": "0.6.4",
       "version": "0.6.4",
       "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz",
       "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz",
@@ -6025,6 +6057,14 @@
         "react-dom": "^16.8.4 || ^17.0.0"
         "react-dom": "^16.8.4 || ^17.0.0"
       }
       }
     },
     },
+    "node_modules/docusaurus-plugin-openapi/node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": {
     "node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": {
       "version": "9.1.0",
       "version": "9.1.0",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -6098,6 +6138,14 @@
         "react-dom": "^16.8.4 || ^17.0.0"
         "react-dom": "^16.8.4 || ^17.0.0"
       }
       }
     },
     },
+    "node_modules/docusaurus-theme-openapi/node_modules/clsx": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/dom-converter": {
     "node_modules/dom-converter": {
       "version": "0.2.0",
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
       "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
@@ -10801,15 +10849,15 @@
       }
       }
     },
     },
     "node_modules/prettier": {
     "node_modules/prettier": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
-      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
+      "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
       "dev": true,
       "dev": true,
       "bin": {
       "bin": {
-        "prettier": "bin-prettier.js"
+        "prettier": "bin/prettier.cjs"
       },
       },
       "engines": {
       "engines": {
-        "node": ">=10.13.0"
+        "node": ">=14"
       },
       },
       "funding": {
       "funding": {
         "url": "https://github.com/prettier/prettier?sponsor=1"
         "url": "https://github.com/prettier/prettier?sponsor=1"
@@ -16745,6 +16793,13 @@
         "rtlcss": "^3.5.0",
         "rtlcss": "^3.5.0",
         "tslib": "^2.4.0",
         "tslib": "^2.4.0",
         "utility-types": "^3.10.0"
         "utility-types": "^3.10.0"
+      },
+      "dependencies": {
+        "clsx": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+          "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+        }
       }
       }
     },
     },
     "@docusaurus/theme-common": {
     "@docusaurus/theme-common": {
@@ -16768,6 +16823,13 @@
         "tslib": "^2.4.0",
         "tslib": "^2.4.0",
         "use-sync-external-store": "^1.2.0",
         "use-sync-external-store": "^1.2.0",
         "utility-types": "^3.10.0"
         "utility-types": "^3.10.0"
+      },
+      "dependencies": {
+        "clsx": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+          "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+        }
       }
       }
     },
     },
     "@docusaurus/theme-search-algolia": {
     "@docusaurus/theme-search-algolia": {
@@ -16791,6 +16853,13 @@
         "lodash": "^4.17.21",
         "lodash": "^4.17.21",
         "tslib": "^2.4.0",
         "tslib": "^2.4.0",
         "utility-types": "^3.10.0"
         "utility-types": "^3.10.0"
+      },
+      "dependencies": {
+        "clsx": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+          "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+        }
       }
       }
     },
     },
     "@docusaurus/theme-translations": {
     "@docusaurus/theme-translations": {
@@ -18515,9 +18584,9 @@
       }
       }
     },
     },
     "clsx": {
     "clsx": {
-      "version": "1.2.1",
-      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
-      "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
+      "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q=="
     },
     },
     "collapse-white-space": {
     "collapse-white-space": {
       "version": "1.0.6",
       "version": "1.0.6",
@@ -19243,6 +19312,13 @@
         "to-vfile": "^6.1.0",
         "to-vfile": "^6.1.0",
         "unified": "^9.0.0",
         "unified": "^9.0.0",
         "unist-util-is": "^4.0.2"
         "unist-util-is": "^4.0.2"
+      },
+      "dependencies": {
+        "clsx": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+          "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+        }
       }
       }
     },
     },
     "docusaurus-plugin-openapi": {
     "docusaurus-plugin-openapi": {
@@ -19268,6 +19344,11 @@
         "webpack": "^5.73.0"
         "webpack": "^5.73.0"
       },
       },
       "dependencies": {
       "dependencies": {
+        "clsx": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+          "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+        },
         "fs-extra": {
         "fs-extra": {
           "version": "9.1.0",
           "version": "9.1.0",
           "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
           "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
@@ -19321,6 +19402,13 @@
         "react-redux": "^7.2.0",
         "react-redux": "^7.2.0",
         "redux-devtools-extension": "^2.13.8",
         "redux-devtools-extension": "^2.13.8",
         "webpack": "^5.73.0"
         "webpack": "^5.73.0"
+      },
+      "dependencies": {
+        "clsx": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+          "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+        }
       }
       }
     },
     },
     "dom-converter": {
     "dom-converter": {
@@ -22663,9 +22751,9 @@
       "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
       "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA=="
     },
     },
     "prettier": {
     "prettier": {
-      "version": "2.8.8",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
-      "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz",
+      "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==",
       "dev": true
       "dev": true
     },
     },
     "pretty-error": {
     "pretty-error": {

+ 2 - 2
docs/package.json

@@ -24,7 +24,7 @@
     "@mdx-js/react": "^1.6.22",
     "@mdx-js/react": "^1.6.22",
     "autoprefixer": "^10.4.13",
     "autoprefixer": "^10.4.13",
     "classnames": "^2.3.2",
     "classnames": "^2.3.2",
-    "clsx": "^1.2.1",
+    "clsx": "^2.0.0",
     "docusaurus-lunr-search": "^2.3.2",
     "docusaurus-lunr-search": "^2.3.2",
     "docusaurus-preset-openapi": "^0.6.3",
     "docusaurus-preset-openapi": "^0.6.3",
     "postcss": "^8.4.25",
     "postcss": "^8.4.25",
@@ -37,7 +37,7 @@
   "devDependencies": {
   "devDependencies": {
     "@docusaurus/module-type-aliases": "^2.4.1",
     "@docusaurus/module-type-aliases": "^2.4.1",
     "@tsconfig/docusaurus": "^1.0.5",
     "@tsconfig/docusaurus": "^1.0.5",
-    "prettier": "^2.8.8",
+    "prettier": "^3.0.0",
     "typescript": "^5.1.6"
     "typescript": "^5.1.6"
   },
   },
   "browserslist": {
   "browserslist": {

+ 9 - 3
docs/src/theme/SearchBar/algolia.css

@@ -61,8 +61,12 @@
 .searchbox__input {
 .searchbox__input {
   display: inline-block;
   display: inline-block;
   box-sizing: border-box;
   box-sizing: border-box;
-  -webkit-transition: box-shadow 0.4s ease, background 0.4s ease;
-  transition: box-shadow 0.4s ease, background 0.4s ease;
+  -webkit-transition:
+    box-shadow 0.4s ease,
+    background 0.4s ease;
+  transition:
+    box-shadow 0.4s ease,
+    background 0.4s ease;
   border: 0;
   border: 0;
   border-radius: 16px;
   border-radius: 16px;
   box-shadow: inset 0 0 0 1px #cccccc;
   box-shadow: inset 0 0 0 1px #cccccc;
@@ -243,7 +247,9 @@
 }
 }
 
 
 .algolia-autocomplete .ds-dropdown-menu {
 .algolia-autocomplete .ds-dropdown-menu {
-  box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1);
+  box-shadow:
+    0 1px 0 0 rgba(0, 0, 0, 0.2),
+    0 2px 3px 0 rgba(0, 0, 0, 0.1);
 }
 }
 
 
 @media (min-width: 601px) {
 @media (min-width: 601px) {

+ 2 - 2
machine-learning/Dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.11-bookworm as builder
+FROM python:3.11-bookworm@sha256:e5a1b0a194a5fbf94f6e350b31c9a508723f9eeb2f9e9e32c3b65df8520a40cc as builder
 
 
 ENV PYTHONDONTWRITEBYTECODE=1 \
 ENV PYTHONDONTWRITEBYTECODE=1 \
   PYTHONUNBUFFERED=1 \
   PYTHONUNBUFFERED=1 \
@@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
 COPY poetry.lock pyproject.toml ./
 COPY poetry.lock pyproject.toml ./
 RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
 RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
 
 
-FROM python:3.11-slim-bookworm
+FROM python:3.11-slim-bookworm@sha256:1bc6a3e9356d64ea632791653bc71a56340e8741dab66434ab2739ebf6aed29d
 
 
 RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
 RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
 
 

+ 1 - 1
machine-learning/export/Dockerfile

@@ -1,4 +1,4 @@
-FROM mambaorg/micromamba:bookworm-slim as builder
+FROM mambaorg/micromamba:bookworm-slim@sha256:d20c621f3ae42f50f380166b15b6c88b14fa62ab6ea188f2cef33451d64057c7 as builder
 
 
 ENV NODE_ENV=production \
 ENV NODE_ENV=production \
   TRANSFORMERS_CACHE=/cache \
   TRANSFORMERS_CACHE=/cache \

+ 0 - 1
mobile/analysis_options.yaml

@@ -49,7 +49,6 @@ dart_code_metrics:
     # Common
     # Common
     - avoid-accessing-collections-by-constant-index
     - avoid-accessing-collections-by-constant-index
     - avoid-accessing-other-classes-private-members
     - avoid-accessing-other-classes-private-members
-    - avoid-async-call-in-sync-function
     - avoid-cascade-after-if-null
     - avoid-cascade-after-if-null
     - avoid-collapsible-if
     - avoid-collapsible-if
     - avoid-collection-methods-with-unrelated-types
     - avoid-collection-methods-with-unrelated-types

+ 1 - 1
mobile/integration_test/test_utils/general_helper.dart

@@ -45,7 +45,7 @@ class ImmichTestHelper {
     await tester.pumpWidget(
     await tester.pumpWidget(
       ProviderScope(
       ProviderScope(
         overrides: [dbProvider.overrideWithValue(db)],
         overrides: [dbProvider.overrideWithValue(db)],
-        child: app.getMainWidget(),
+        child: const app.MainWidget(),
       ),
       ),
     );
     );
     // Post run tasks
     // Post run tasks

+ 3 - 3
mobile/lib/constants/immich_colors.dart

@@ -1,5 +1,5 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 
 
-Color immichBackgroundColor = const Color(0xFFf6f8fe);
-Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0);
-Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250);
+const Color immichBackgroundColor = Color(0xFFf6f8fe);
+const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0);
+const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250);

+ 19 - 9
mobile/lib/extensions/asyncvalue_extensions.dart

@@ -4,22 +4,32 @@ import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 
 
-extension ScaffoldBody<T> on AsyncValue<T> {
-  static final Logger _scaffoldBodyLog = Logger("ScaffoldBody");
+extension LogOnError<T> on AsyncValue<T> {
+  static final Logger _asyncErrorLogger = Logger("AsyncValue");
 
 
-  Widget scaffoldBodyWhen({
+  Widget widgetWhen({
+    bool skipLoadingOnRefresh = true,
+    Widget Function()? onLoading,
+    Widget Function(Object? error, StackTrace? stack)? onError,
     required Widget Function(T data) onData,
     required Widget Function(T data) onData,
-    Widget? onError,
   }) {
   }) {
     if (isLoading) {
     if (isLoading) {
-      return const Center(
-        child: ImmichLoadingIndicator(),
-      );
+      bool skip = false;
+      if (isRefreshing) {
+        skip = skipLoadingOnRefresh;
+      }
+
+      if (!skip) {
+        return onLoading?.call() ??
+            const Center(
+              child: ImmichLoadingIndicator(),
+            );
+      }
     }
     }
 
 
     if (hasError && !hasValue) {
     if (hasError && !hasValue) {
-      _scaffoldBodyLog.severe("Error occured in AsyncValue", error, stackTrace);
-      return onError ?? const ScaffoldErrorBody();
+      _asyncErrorLogger.severe("Error occured", error, stackTrace);
+      return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody();
     }
     }
 
 
     return onData(requireValue);
     return onData(requireValue);

+ 1 - 1
mobile/lib/extensions/build_context_extensions.dart

@@ -45,7 +45,7 @@ extension ContextHelper on BuildContext {
   ) =>
   ) =>
       AutoRouter.of(this).navigate(route);
       AutoRouter.of(this).navigate(route);
 
 
-// Auto-Push replace route from the current context
+  // Auto-Push replace route from the current context
   Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
   Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
       AutoRouter.of(this).replace(route);
       AutoRouter.of(this).replace(route);
 
 

+ 33 - 31
mobile/lib/main.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:device_info_plus/device_info_plus.dart';
 import 'package:device_info_plus/device_info_plus.dart';
@@ -7,6 +8,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:timezone/data/latest.dart';
 import 'package:timezone/data/latest.dart';
 import 'package:immich_mobile/constants/locales.dart';
 import 'package:immich_mobile/constants/locales.dart';
 import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
 import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
@@ -28,7 +30,6 @@ import 'package:immich_mobile/shared/providers/app_state.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/immich_logger.service.dart';
 import 'package:immich_mobile/shared/services/immich_logger.service.dart';
 import 'package:immich_mobile/shared/services/local_notification.service.dart';
 import 'package:immich_mobile/shared/services/local_notification.service.dart';
-import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
 import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
 import 'package:immich_mobile/utils/immich_app_theme.dart';
 import 'package:immich_mobile/utils/immich_app_theme.dart';
 import 'package:immich_mobile/utils/migration.dart';
 import 'package:immich_mobile/utils/migration.dart';
@@ -43,10 +44,11 @@ void main() async {
   await initApp();
   await initApp();
   await migrateDatabaseIfNeeded(db);
   await migrateDatabaseIfNeeded(db);
   HttpOverrides.global = HttpSSLCertOverride();
   HttpOverrides.global = HttpSSLCertOverride();
+
   runApp(
   runApp(
     ProviderScope(
     ProviderScope(
       overrides: [dbProvider.overrideWithValue(db)],
       overrides: [dbProvider.overrideWithValue(db)],
-      child: getMainWidget(),
+      child: const MainWidget(),
     ),
     ),
   );
   );
 }
 }
@@ -108,16 +110,6 @@ Future<Isar> loadDb() async {
   return db;
   return db;
 }
 }
 
 
-Widget getMainWidget() {
-  return EasyLocalization(
-    supportedLocales: locales,
-    path: translationsPath,
-    useFallbackTranslations: true,
-    fallbackLocale: locales.first,
-    child: const ImmichApp(),
-  );
-}
-
 class ImmichApp extends ConsumerStatefulWidget {
 class ImmichApp extends ConsumerStatefulWidget {
   const ImmichApp({super.key});
   const ImmichApp({super.key});
 
 
@@ -167,10 +159,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
       // Android 8 does not support transparent app bars
       // Android 8 does not support transparent app bars
       final info = await DeviceInfoPlugin().androidInfo;
       final info = await DeviceInfoPlugin().androidInfo;
       if (info.version.sdkInt <= 26) {
       if (info.version.sdkInt <= 26) {
-        overlayStyle =
-            MediaQuery.of(context).platformBrightness == Brightness.light
-                ? SystemUiOverlayStyle.light
-                : SystemUiOverlayStyle.dark;
+        overlayStyle = context.isDarkTheme
+            ? SystemUiOverlayStyle.dark
+            : SystemUiOverlayStyle.light;
       }
       }
     }
     }
     SystemChrome.setSystemUIOverlayStyle(overlayStyle);
     SystemChrome.setSystemUIOverlayStyle(overlayStyle);
@@ -202,22 +193,33 @@ class ImmichAppState extends ConsumerState<ImmichApp>
       supportedLocales: context.supportedLocales,
       supportedLocales: context.supportedLocales,
       locale: context.locale,
       locale: context.locale,
       debugShowCheckedModeBanner: false,
       debugShowCheckedModeBanner: false,
-      home: Stack(
-        children: [
-          MaterialApp.router(
-            title: 'Immich',
-            debugShowCheckedModeBanner: false,
-            themeMode: ref.watch(immichThemeProvider),
-            darkTheme: immichDarkTheme,
-            theme: immichLightTheme,
-            routeInformationParser: router.defaultRouteParser(),
-            routerDelegate: router.delegate(
-              navigatorObservers: () => [TabNavigationObserver(ref: ref)],
-            ),
-          ),
-          const ImmichLoadingOverlay(),
-        ],
+      home: MaterialApp.router(
+        title: 'Immich',
+        debugShowCheckedModeBanner: false,
+        themeMode: ref.watch(immichThemeProvider),
+        darkTheme: immichDarkTheme,
+        theme: immichLightTheme,
+        routeInformationParser: router.defaultRouteParser(),
+        routerDelegate: router.delegate(
+          navigatorObservers: () => [TabNavigationObserver(ref: ref)],
+        ),
       ),
       ),
     );
     );
   }
   }
 }
 }
+
+// ignore: prefer-single-widget-per-file
+class MainWidget extends StatelessWidget {
+  const MainWidget({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return EasyLocalization(
+      supportedLocales: locales,
+      path: translationsPath,
+      useFallbackTranslations: true,
+      fallbackLocale: locales.first,
+      child: const ImmichApp(),
+    );
+  }
+}

+ 4 - 7
mobile/lib/modules/activities/views/activities_page.dart

@@ -4,12 +4,12 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
 import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/extensions/datetime_extensions.dart';
 import 'package:immich_mobile/extensions/datetime_extensions.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
@@ -88,7 +88,7 @@ class ActivitiesPage extends HookConsumerWidget {
               width: 40,
               width: 40,
               height: 30,
               height: 30,
               decoration: BoxDecoration(
               decoration: BoxDecoration(
-                borderRadius: BorderRadius.circular(4),
+                borderRadius: const BorderRadius.all(Radius.circular(4)),
                 image: DecorationImage(
                 image: DecorationImage(
                   image: CachedNetworkImageProvider(
                   image: CachedNetworkImageProvider(
                     getThumbnailUrlForRemoteId(
                     getThumbnailUrlForRemoteId(
@@ -231,11 +231,8 @@ class ActivitiesPage extends HookConsumerWidget {
 
 
     return Scaffold(
     return Scaffold(
       appBar: AppBar(title: Text(appBarTitle)),
       appBar: AppBar(title: Text(appBarTitle)),
-      body: activities.maybeWhen(
-        orElse: () {
-          return const Center(child: ImmichLoadingIndicator());
-        },
-        data: (data) {
+      body: activities.widgetWhen(
+        onData: (data) {
           final liked = data.firstWhereOrNull(
           final liked = data.firstWhereOrNull(
             (a) =>
             (a) =>
                 a.type == ActivityType.like &&
                 a.type == ActivityType.like &&

+ 1 - 1
mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart

@@ -65,7 +65,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
       }
       }
 
 
       ref.invalidate(albumDetailProvider(album.id));
       ref.invalidate(albumDetailProvider(album.id));
-      Navigator.pop(context);
+      context.pop();
     }
     }
 
 
     return Card(
     return Card(

+ 17 - 16
mobile/lib/modules/album/ui/album_viewer_appbar.dart

@@ -43,6 +43,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
   Widget build(BuildContext context, WidgetRef ref) {
   Widget build(BuildContext context, WidgetRef ref) {
     final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
     final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
     final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
     final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
+    final isProcessing = useProcessingOverlay();
     final comments = album.shared
     final comments = album.shared
         ? ref.watch(
         ? ref.watch(
             activityStatisticsStateProvider(
             activityStatisticsStateProvider(
@@ -52,7 +53,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         : 0;
         : 0;
 
 
     deleteAlbum() async {
     deleteAlbum() async {
-      ImmichLoadingOverlayController.appLoader.show();
+      isProcessing.value = true;
 
 
       final bool success;
       final bool success;
       if (album.shared) {
       if (album.shared) {
@@ -74,7 +75,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         );
         );
       }
       }
 
 
-      ImmichLoadingOverlayController.appLoader.hide();
+      isProcessing.value = false;
     }
     }
 
 
     Future<void> showConfirmationDialog() async {
     Future<void> showConfirmationDialog() async {
@@ -89,7 +90,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
             ),
             ),
             actions: <Widget>[
             actions: <Widget>[
               TextButton(
               TextButton(
-                onPressed: () => Navigator.pop(context, 'Cancel'),
+                onPressed: () => context.pop('Cancel'),
                 child: Text(
                 child: Text(
                   'Cancel',
                   'Cancel',
                   style: TextStyle(
                   style: TextStyle(
@@ -100,7 +101,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
               ),
               ),
               TextButton(
               TextButton(
                 onPressed: () {
                 onPressed: () {
-                  Navigator.pop(context, 'Confirm');
+                  context.pop('Confirm');
                   deleteAlbum();
                   deleteAlbum();
                 },
                 },
                 child: Text(
                 child: Text(
@@ -122,7 +123,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
     }
     }
 
 
     void onLeaveAlbumPressed() async {
     void onLeaveAlbumPressed() async {
-      ImmichLoadingOverlayController.appLoader.show();
+      isProcessing.value = true;
 
 
       bool isSuccess =
       bool isSuccess =
           await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
           await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
@@ -131,7 +132,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         context
         context
             .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
             .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
       } else {
       } else {
-        Navigator.pop(context);
+        context.pop();
         ImmichToast.show(
         ImmichToast.show(
           context: context,
           context: context,
           msg: "album_viewer_appbar_share_err_leave".tr(),
           msg: "album_viewer_appbar_share_err_leave".tr(),
@@ -140,11 +141,11 @@ class AlbumViewerAppbar extends HookConsumerWidget
         );
         );
       }
       }
 
 
-      ImmichLoadingOverlayController.appLoader.hide();
+      isProcessing.value = false;
     }
     }
 
 
     void onRemoveFromAlbumPressed() async {
     void onRemoveFromAlbumPressed() async {
-      ImmichLoadingOverlayController.appLoader.show();
+      isProcessing.value = true;
 
 
       bool isSuccess =
       bool isSuccess =
           await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
           await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
@@ -153,12 +154,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
               );
               );
 
 
       if (isSuccess) {
       if (isSuccess) {
-        Navigator.pop(context);
+        context.pop();
         selectionDisabled();
         selectionDisabled();
         ref.watch(albumProvider.notifier).getAllAlbums();
         ref.watch(albumProvider.notifier).getAllAlbums();
         ref.invalidate(albumDetailProvider(album.id));
         ref.invalidate(albumDetailProvider(album.id));
       } else {
       } else {
-        Navigator.pop(context);
+        context.pop();
         ImmichToast.show(
         ImmichToast.show(
           context: context,
           context: context,
           msg: "album_viewer_appbar_share_err_remove".tr(),
           msg: "album_viewer_appbar_share_err_remove".tr(),
@@ -167,7 +168,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         );
         );
       }
       }
 
 
-      ImmichLoadingOverlayController.appLoader.hide();
+      isProcessing.value = false;
     }
     }
 
 
     void handleShareAssets(
     void handleShareAssets(
@@ -198,9 +199,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
     }
     }
 
 
     void onShareAssetsTo() async {
     void onShareAssetsTo() async {
-      ImmichLoadingOverlayController.appLoader.show();
+      isProcessing.value = true;
       handleShareAssets(ref, context, selected);
       handleShareAssets(ref, context, selected);
-      ImmichLoadingOverlayController.appLoader.hide();
+      isProcessing.value = false;
     }
     }
 
 
     buildBottomSheetActions() {
     buildBottomSheetActions() {
@@ -253,7 +254,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ListTile(
         ListTile(
           leading: const Icon(Icons.person_add_alt_rounded),
           leading: const Icon(Icons.person_add_alt_rounded),
           onTap: () {
           onTap: () {
-            Navigator.pop(context);
+            context.pop();
             onAddUsers!(album);
             onAddUsers!(album);
           },
           },
           title: const Text(
           title: const Text(
@@ -265,7 +266,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
           leading: const Icon(Icons.share_rounded),
           leading: const Icon(Icons.share_rounded),
           onTap: () {
           onTap: () {
             context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
             context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
-            Navigator.pop(context);
+            context.pop();
           },
           },
           title: const Text(
           title: const Text(
             "control_bottom_app_bar_share",
             "control_bottom_app_bar_share",
@@ -286,7 +287,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ListTile(
         ListTile(
           leading: const Icon(Icons.add_photo_alternate_outlined),
           leading: const Icon(Icons.add_photo_alternate_outlined),
           onTap: () {
           onTap: () {
-            Navigator.pop(context);
+            context.pop();
             onAddPhotos!(album);
             onAddPhotos!(album);
           },
           },
           title: const Text(
           title: const Text(

+ 8 - 9
mobile/lib/modules/album/views/album_options_part.dart

@@ -24,10 +24,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
     final owner = album.owner.value;
     final owner = album.owner.value;
     final userId = ref.watch(authenticationProvider).userId;
     final userId = ref.watch(authenticationProvider).userId;
     final activityEnabled = useState(album.activityEnabled);
     final activityEnabled = useState(album.activityEnabled);
+    final isProcessing = useProcessingOverlay();
     final isOwner = owner?.id == userId;
     final isOwner = owner?.id == userId;
 
 
     void showErrorMessage() {
     void showErrorMessage() {
-      Navigator.pop(context);
+      context.pop();
       ImmichToast.show(
       ImmichToast.show(
         context: context,
         context: context,
         msg: "shared_album_section_people_action_error".tr(),
         msg: "shared_album_section_people_action_error".tr(),
@@ -37,7 +38,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
     }
     }
 
 
     void leaveAlbum() async {
     void leaveAlbum() async {
-      ImmichLoadingOverlayController.appLoader.show();
+      isProcessing.value = true;
 
 
       try {
       try {
         final isSuccess =
         final isSuccess =
@@ -54,11 +55,11 @@ class AlbumOptionsPage extends HookConsumerWidget {
         showErrorMessage();
         showErrorMessage();
       }
       }
 
 
-      ImmichLoadingOverlayController.appLoader.hide();
+      isProcessing.value = false;
     }
     }
 
 
     void removeUserFromAlbum(User user) async {
     void removeUserFromAlbum(User user) async {
-      ImmichLoadingOverlayController.appLoader.show();
+      isProcessing.value = true;
 
 
       try {
       try {
         await ref
         await ref
@@ -70,8 +71,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
         showErrorMessage();
         showErrorMessage();
       }
       }
 
 
-      Navigator.pop(context);
-      ImmichLoadingOverlayController.appLoader.hide();
+      context.pop();
+      isProcessing.value = false;
     }
     }
 
 
     void handleUserClick(User user) {
     void handleUserClick(User user) {
@@ -180,9 +181,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
       appBar: AppBar(
       appBar: AppBar(
         leading: IconButton(
         leading: IconButton(
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
-          onPressed: () {
-            context.autoPop(null);
-          },
+          onPressed: () => context.autoPop(null),
         ),
         ),
         centerTitle: true,
         centerTitle: true,
         title: Text("translated_text_options".tr()),
         title: Text("translated_text_options".tr()),

+ 18 - 26
mobile/lib/modules/album/views/album_viewer_page.dart

@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
@@ -17,7 +18,6 @@ import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 
 
@@ -33,6 +33,7 @@ class AlbumViewerPage extends HookConsumerWidget {
     final userId = ref.watch(authenticationProvider).userId;
     final userId = ref.watch(authenticationProvider).userId;
     final selection = useState<Set<Asset>>({});
     final selection = useState<Set<Asset>>({});
     final multiSelectEnabled = useState(false);
     final multiSelectEnabled = useState(false);
+    final isProcessing = useProcessingOverlay();
 
 
     useEffect(
     useEffect(
       () {
       () {
@@ -75,24 +76,21 @@ class AlbumViewerPage extends HookConsumerWidget {
         ),
         ),
       );
       );
 
 
-      if (returnPayload != null) {
+      if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) {
         // Check if there is new assets add
         // Check if there is new assets add
-        if (returnPayload.selectedAssets.isNotEmpty) {
-          ImmichLoadingOverlayController.appLoader.show();
+        isProcessing.value = true;
 
 
-          var addAssetsResult =
-              await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
-                    returnPayload.selectedAssets,
-                    albumInfo,
-                  );
+        var addAssetsResult =
+            await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
+                  returnPayload.selectedAssets,
+                  albumInfo,
+                );
 
 
-          if (addAssetsResult != null &&
-              addAssetsResult.successfullyAdded > 0) {
-            ref.invalidate(albumDetailProvider(albumId));
-          }
-
-          ImmichLoadingOverlayController.appLoader.hide();
+        if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) {
+          ref.invalidate(albumDetailProvider(albumId));
         }
         }
+
+        isProcessing.value = false;
       }
       }
     }
     }
 
 
@@ -102,7 +100,7 @@ class AlbumViewerPage extends HookConsumerWidget {
       );
       );
 
 
       if (sharedUserIds != null) {
       if (sharedUserIds != null) {
-        ImmichLoadingOverlayController.appLoader.show();
+        isProcessing.value = true;
 
 
         var isSuccess = await ref
         var isSuccess = await ref
             .watch(albumServiceProvider)
             .watch(albumServiceProvider)
@@ -112,7 +110,7 @@ class AlbumViewerPage extends HookConsumerWidget {
           ref.invalidate(albumDetailProvider(album.id));
           ref.invalidate(albumDetailProvider(album.id));
         }
         }
 
 
-        ImmichLoadingOverlayController.appLoader.hide();
+        isProcessing.value = false;
       }
       }
     }
     }
 
 
@@ -260,13 +258,11 @@ class AlbumViewerPage extends HookConsumerWidget {
         error: (error, stackTrace) => AppBar(title: const Text("Error")),
         error: (error, stackTrace) => AppBar(title: const Text("Error")),
         loading: () => AppBar(),
         loading: () => AppBar(),
       ),
       ),
-      body: album.when(
-        data: (data) => WillPopScope(
+      body: album.widgetWhen(
+        onData: (data) => WillPopScope(
           onWillPop: onWillPop,
           onWillPop: onWillPop,
           child: GestureDetector(
           child: GestureDetector(
-            onTap: () {
-              titleFocusNode.unfocus();
-            },
+            onTap: () => titleFocusNode.unfocus(),
             child: ImmichAssetGrid(
             child: ImmichAssetGrid(
               renderList: data.renderList,
               renderList: data.renderList,
               listener: selectionListener,
               listener: selectionListener,
@@ -285,10 +281,6 @@ class AlbumViewerPage extends HookConsumerWidget {
             ),
             ),
           ),
           ),
         ),
         ),
-        error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
       ),
     );
     );
   }
   }

+ 3 - 6
mobile/lib/modules/album/views/asset_selection_page.dart

@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
@@ -85,12 +86,8 @@ class AssetSelectionPage extends HookConsumerWidget {
             ),
             ),
         ],
         ],
       ),
       ),
-      body: renderList.when(
-        data: (data) => buildBody(data),
-        error: (error, stackTrace) => Center(
-          child: Text(error.toString()),
-        ),
-        loading: () => const Center(child: CircularProgressIndicator()),
+      body: renderList.widgetWhen(
+        onData: (data) => buildBody(data),
       ),
       ),
     );
     );
   }
   }

+ 3 - 7
mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart

@@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
 import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 
 
 class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
@@ -137,8 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
           ),
           ),
         ],
         ],
       ),
       ),
-      body: suggestedShareUsers.when(
-        data: (users) {
+      body: suggestedShareUsers.widgetWhen(
+        onData: (users) {
           for (var sharedUsers in album.sharedUsers) {
           for (var sharedUsers in album.sharedUsers) {
             users.removeWhere(
             users.removeWhere(
               (u) => u.id == sharedUsers.id || u.id == album.ownerId,
               (u) => u.id == sharedUsers.id || u.id == album.ownerId,
@@ -147,10 +147,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
 
 
           return buildUserList(users);
           return buildUserList(users);
         },
         },
-        error: (e, _) => Text("Error loading suggested users $e"),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
       ),
     );
     );
   }
   }

+ 9 - 8
mobile/lib/modules/album/views/select_user_for_sharing_page.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -9,7 +10,6 @@ import 'package:immich_mobile/modules/album/providers/suggested_shared_users.pro
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 
 
 class SelectUserForSharingPage extends HookConsumerWidget {
 class SelectUserForSharingPage extends HookConsumerWidget {
@@ -42,7 +42,12 @@ class SelectUserForSharingPage extends HookConsumerWidget {
 
 
       ScaffoldMessenger(
       ScaffoldMessenger(
         child: SnackBar(
         child: SnackBar(
-          content: const Text('select_user_for_sharing_page_err_album').tr(),
+          content: Text(
+            'select_user_for_sharing_page_err_album',
+            style: context.textTheme.bodyLarge?.copyWith(
+              color: context.primaryColor,
+            ),
+          ).tr(),
         ),
         ),
       );
       );
     }
     }
@@ -166,14 +171,10 @@ class SelectUserForSharingPage extends HookConsumerWidget {
           ),
           ),
         ],
         ],
       ),
       ),
-      body: suggestedShareUsers.when(
-        data: (users) {
+      body: suggestedShareUsers.widgetWhen(
+        onData: (users) {
           return buildUserList(users);
           return buildUserList(users);
         },
         },
-        error: (e, _) => Text("Error loading suggested users $e"),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
       ),
     );
     );
   }
   }

+ 34 - 42
mobile/lib/modules/archive/views/archive_page.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
 import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@@ -48,37 +49,33 @@ class ArchivePage extends HookConsumerWidget {
           child: SizedBox(
           child: SizedBox(
             height: 64,
             height: 64,
             child: Card(
             child: Card(
-              child: Column(
-                children: [
-                  ListTile(
-                    shape: RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(10),
-                    ),
-                    leading: const Icon(
-                      Icons.unarchive_rounded,
-                    ),
-                    title: Text(
-                      'control_bottom_app_bar_unarchive'.tr(),
-                      style: const TextStyle(fontSize: 14),
-                    ),
-                    onTap: processing.value
-                        ? null
-                        : () async {
-                            processing.value = true;
-                            try {
-                              await handleArchiveAssets(
-                                ref,
-                                context,
-                                selection.value.toList(),
-                                shouldArchive: false,
-                              );
-                            } finally {
-                              processing.value = false;
-                              selectionEnabledHook.value = false;
-                            }
-                          },
-                  ),
-                ],
+              child: ListTile(
+                shape: const RoundedRectangleBorder(
+                  borderRadius: BorderRadius.all(Radius.circular(10)),
+                ),
+                leading: const Icon(
+                  Icons.unarchive_rounded,
+                ),
+                title: Text(
+                  'control_bottom_app_bar_unarchive'.tr(),
+                  style: const TextStyle(fontSize: 14),
+                ),
+                onTap: processing.value
+                    ? null
+                    : () async {
+                        processing.value = true;
+                        try {
+                          await handleArchiveAssets(
+                            ref,
+                            context,
+                            selection.value.toList(),
+                            shouldArchive: false,
+                          );
+                        } finally {
+                          processing.value = false;
+                          selectionEnabledHook.value = false;
+                        }
+                      },
               ),
               ),
             ),
             ),
           ),
           ),
@@ -86,18 +83,13 @@ class ArchivePage extends HookConsumerWidget {
       );
       );
     }
     }
 
 
-    return archivedAssets.when(
-      loading: () => Scaffold(
-        appBar: buildAppBar("?"),
-        body: const Center(child: CircularProgressIndicator()),
-      ),
-      error: (error, stackTrace) => Scaffold(
-        appBar: buildAppBar("Error"),
-        body: Center(child: Text(error.toString())),
+    return Scaffold(
+      appBar: archivedAssets.maybeWhen(
+        data: (data) => buildAppBar(data.totalAssets.toString()),
+        orElse: () => buildAppBar("?"),
       ),
       ),
-      data: (data) => Scaffold(
-        appBar: buildAppBar(data.totalAssets.toString()),
-        body: data.isEmpty
+      body: archivedAssets.widgetWhen(
+        onData: (data) => data.isEmpty
             ? Center(
             ? Center(
                 child: Text('archive_page_no_archived_assets'.tr()),
                 child: Text('archive_page_no_archived_assets'.tr()),
               )
               )

+ 8 - 2
mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart

@@ -62,8 +62,14 @@ class AdvancedBottomSheet extends HookConsumerWidget {
                                   ClipboardData(text: assetDetail.toString()),
                                   ClipboardData(text: assetDetail.toString()),
                                 ).then((_) {
                                 ).then((_) {
                                   ScaffoldMessenger.of(context).showSnackBar(
                                   ScaffoldMessenger.of(context).showSnackBar(
-                                    const SnackBar(
-                                      content: Text("Copied to clipboard"),
+                                    SnackBar(
+                                      content: Text(
+                                        "Copied to clipboard",
+                                        style: context.textTheme.bodyLarge
+                                            ?.copyWith(
+                                          color: context.primaryColor,
+                                        ),
+                                      ),
                                     ),
                                     ),
                                   );
                                   );
                                 });
                                 });

+ 4 - 4
mobile/lib/modules/asset_viewer/views/gallery_viewer.dart

@@ -514,7 +514,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                               currentAsset,
                               currentAsset,
                               stackElements.elementAt(stackIndex.value),
                               stackElements.elementAt(stackIndex.value),
                             );
                             );
-                        Navigator.pop(ctx);
+                        ctx.pop();
                         context.autoPop();
                         context.autoPop();
                       },
                       },
                       title: const Text(
                       title: const Text(
@@ -541,7 +541,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                           stackElements.elementAt(1),
                           stackElements.elementAt(1),
                           childrenToRemove: [currentAsset],
                           childrenToRemove: [currentAsset],
                         );
                         );
-                        Navigator.pop(ctx);
+                        ctx.pop();
                         context.autoPop();
                         context.autoPop();
                       } else {
                       } else {
                         await ref.read(assetStackServiceProvider).updateStack(
                         await ref.read(assetStackServiceProvider).updateStack(
@@ -551,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                           ],
                           ],
                         );
                         );
                         removeAssetFromStack();
                         removeAssetFromStack();
-                        Navigator.pop(ctx);
+                        ctx.pop();
                       }
                       }
                     },
                     },
                     title: const Text(
                     title: const Text(
@@ -569,7 +569,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                             currentAsset,
                             currentAsset,
                             childrenToRemove: stack,
                             childrenToRemove: stack,
                           );
                           );
-                      Navigator.pop(ctx);
+                      ctx.pop();
                       context.autoPop();
                       context.autoPop();
                     },
                     },
                     title: const Text(
                     title: const Text(

+ 3 - 0
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -229,6 +229,9 @@ class BackupControllerPage extends HookConsumerWidget {
       final snackBar = SnackBar(
       final snackBar = SnackBar(
         content: Text(
         content: Text(
           msg.tr(),
           msg.tr(),
+          style: context.textTheme.bodyLarge?.copyWith(
+            color: context.primaryColor,
+          ),
         ),
         ),
         backgroundColor: Colors.red,
         backgroundColor: Colors.red,
       );
       );

+ 15 - 20
mobile/lib/modules/favorite/views/favorites_page.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
@@ -62,22 +63,18 @@ class FavoritesPage extends HookConsumerWidget {
           child: SizedBox(
           child: SizedBox(
             height: 64,
             height: 64,
             child: Card(
             child: Card(
-              child: Column(
-                children: [
-                  ListTile(
-                    shape: RoundedRectangleBorder(
-                      borderRadius: BorderRadius.circular(10),
-                    ),
-                    leading: const Icon(
-                      Icons.star_border,
-                    ),
-                    title: const Text(
-                      "Unfavorite",
-                      style: TextStyle(fontSize: 14),
-                    ),
-                    onTap: processing.value ? null : unfavorite,
-                  ),
-                ],
+              child: ListTile(
+                shape: const RoundedRectangleBorder(
+                  borderRadius: BorderRadius.all(Radius.circular(10)),
+                ),
+                leading: const Icon(
+                  Icons.star_border,
+                ),
+                title: const Text(
+                  "Unfavorite",
+                  style: TextStyle(fontSize: 14),
+                ),
+                onTap: processing.value ? null : unfavorite,
               ),
               ),
             ),
             ),
           ),
           ),
@@ -87,10 +84,8 @@ class FavoritesPage extends HookConsumerWidget {
 
 
     return Scaffold(
     return Scaffold(
       appBar: buildAppBar(),
       appBar: buildAppBar(),
-      body: ref.watch(favoriteAssetsProvider).when(
-            loading: () => const Center(child: CircularProgressIndicator()),
-            error: (error, stackTrace) => Center(child: Text(error.toString())),
-            data: (data) => data.isEmpty
+      body: ref.watch(favoriteAssetsProvider).widgetWhen(
+            onData: (data) => data.isEmpty
                 ? Center(
                 ? Center(
                     child: Text('favorites_page_no_favorites'.tr()),
                     child: Text('favorites_page_no_favorites'.tr()),
                   )
                   )

+ 3 - 7
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart

@@ -5,13 +5,13 @@ import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 
 
 class ImmichAssetGrid extends HookConsumerWidget {
 class ImmichAssetGrid extends HookConsumerWidget {
@@ -130,12 +130,8 @@ class ImmichAssetGrid extends HookConsumerWidget {
     if (renderList != null) return buildAssetGridView(renderList!);
     if (renderList != null) return buildAssetGridView(renderList!);
 
 
     final renderListFuture = ref.watch(renderListProvider(assets!));
     final renderListFuture = ref.watch(renderListProvider(assets!));
-    return renderListFuture.when(
-      data: (renderList) => buildAssetGridView(renderList),
-      error: (err, stack) => Center(child: Text("$err")),
-      loading: () => const Center(
-        child: ImmichLoadingIndicator(),
-      ),
+    return renderListFuture.widgetWhen(
+      onData: (renderList) => buildAssetGridView(renderList),
     );
     );
   }
   }
 }
 }

+ 3 - 1
mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart

@@ -197,7 +197,9 @@ class ThumbnailImage extends StatelessWidget {
       },
       },
       child: Stack(
       child: Stack(
         children: [
         children: [
-          Container(
+          AnimatedContainer(
+            duration: const Duration(milliseconds: 300),
+            curve: Curves.decelerate,
             decoration: BoxDecoration(
             decoration: BoxDecoration(
               border: multiselectEnabled && isSelected
               border: multiselectEnabled && isSelected
                   ? Border.all(
                   ? Border.all(

+ 8 - 12
mobile/lib/modules/home/views/home_page.dart

@@ -28,6 +28,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 import 'package:immich_mobile/shared/ui/immich_app_bar.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
+import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 import 'package:immich_mobile/utils/selection_handlers.dart';
 import 'package:immich_mobile/utils/selection_handlers.dart';
 
 
 class HomePage extends HookConsumerWidget {
 class HomePage extends HookConsumerWidget {
@@ -50,7 +51,7 @@ class HomePage extends HookConsumerWidget {
 
 
     final tipOneOpacity = useState(0.0);
     final tipOneOpacity = useState(0.0);
     final refreshCount = useState(0);
     final refreshCount = useState(0);
-    final processing = useState(false);
+    final processing = useProcessingOverlay();
 
 
     useEffect(
     useEffect(
       () {
       () {
@@ -212,10 +213,10 @@ class HomePage extends HookConsumerWidget {
         processing.value = true;
         processing.value = true;
         selectionEnabledHook.value = false;
         selectionEnabledHook.value = false;
         try {
         try {
-          ref.read(manualUploadProvider.notifier).uploadAssets(
-                context,
-                selection.value.where((a) => a.storage == AssetState.local),
-              );
+            ref.read(manualUploadProvider.notifier).uploadAssets(
+                  context,
+                  selection.value.where((a) => a.storage == AssetState.local),
+                );
         } finally {
         } finally {
           processing.value = false;
           processing.value = false;
         }
         }
@@ -323,16 +324,12 @@ class HomePage extends HookConsumerWidget {
         } else {
         } else {
           refreshCount.value++;
           refreshCount.value++;
           // set counter back to 0 if user does not request refresh again
           // set counter back to 0 if user does not request refresh again
-          Timer(const Duration(seconds: 4), () {
-            refreshCount.value = 0;
-          });
+          Timer(const Duration(seconds: 4), () => refreshCount.value = 0);
         }
         }
       }
       }
 
 
       buildLoadingIndicator() {
       buildLoadingIndicator() {
-        Timer(const Duration(seconds: 2), () {
-          tipOneOpacity.value = 1;
-        });
+        Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1);
 
 
         return Center(
         return Center(
           child: Column(
           child: Column(
@@ -415,7 +412,6 @@ class HomePage extends HookConsumerWidget {
                 selectionAssetState: selectionAssetState.value,
                 selectionAssetState: selectionAssetState.value,
                 onStack: onStack,
                 onStack: onStack,
               ),
               ),
-            if (processing.value) const Center(child: ImmichLoadingIndicator()),
           ],
           ],
         ),
         ),
       );
       );

+ 12 - 5
mobile/lib/modules/login/ui/login_form.dart

@@ -48,7 +48,7 @@ class LoginForm extends HookConsumerWidget {
     /// Fetch the server login credential and enables oAuth login if necessary
     /// Fetch the server login credential and enables oAuth login if necessary
     /// Returns true if successful, false otherwise
     /// Returns true if successful, false otherwise
     Future<bool> getServerLoginCredential() async {
     Future<bool> getServerLoginCredential() async {
-      final serverUrl = serverEndpointController.text.trim();
+      final serverUrl = sanitizeUrl(serverEndpointController.text);
 
 
       // Guard empty URL
       // Guard empty URL
       if (serverUrl.isEmpty) {
       if (serverUrl.isEmpty) {
@@ -127,6 +127,12 @@ class LoginForm extends HookConsumerWidget {
     );
     );
 
 
     populateTestLoginInfo() {
     populateTestLoginInfo() {
+      usernameController.text = 'demo@immich.app';
+      passwordController.text = 'demo';
+      serverEndpointController.text = 'https://demo.immich.app';
+    }
+
+    populateTestLoginInfo1() {
       usernameController.text = 'testuser@email.com';
       usernameController.text = 'testuser@email.com';
       passwordController.text = 'password';
       passwordController.text = 'password';
       serverEndpointController.text = 'http://10.1.15.216:2283/api';
       serverEndpointController.text = 'http://10.1.15.216:2283/api';
@@ -144,7 +150,7 @@ class LoginForm extends HookConsumerWidget {
             await ref.read(authenticationProvider.notifier).login(
             await ref.read(authenticationProvider.notifier).login(
                   usernameController.text,
                   usernameController.text,
                   passwordController.text,
                   passwordController.text,
-                  serverEndpointController.text.trim(),
+                  sanitizeUrl(serverEndpointController.text),
                 );
                 );
         if (isAuthenticated) {
         if (isAuthenticated) {
           // Resume backup (if enable) then navigate
           // Resume backup (if enable) then navigate
@@ -181,7 +187,7 @@ class LoginForm extends HookConsumerWidget {
 
 
       try {
       try {
         oAuthServerConfig = await oAuthService
         oAuthServerConfig = await oAuthService
-            .getOAuthServerConfig(serverEndpointController.text);
+            .getOAuthServerConfig(sanitizeUrl(serverEndpointController.text));
 
 
         isLoading.value = true;
         isLoading.value = true;
       } catch (e) {
       } catch (e) {
@@ -203,7 +209,7 @@ class LoginForm extends HookConsumerWidget {
               .watch(authenticationProvider.notifier)
               .watch(authenticationProvider.notifier)
               .setSuccessLoginInfo(
               .setSuccessLoginInfo(
                 accessToken: loginResponseDto.accessToken,
                 accessToken: loginResponseDto.accessToken,
-                serverUrl: serverEndpointController.text,
+                serverUrl: sanitizeUrl(serverEndpointController.text),
               );
               );
 
 
           if (isSuccess) {
           if (isSuccess) {
@@ -299,7 +305,7 @@ class LoginForm extends HookConsumerWidget {
           crossAxisAlignment: CrossAxisAlignment.stretch,
           crossAxisAlignment: CrossAxisAlignment.stretch,
           children: [
           children: [
             Text(
             Text(
-              serverEndpointController.text,
+              sanitizeUrl(serverEndpointController.text),
               style: context.textTheme.displaySmall,
               style: context.textTheme.displaySmall,
               textAlign: TextAlign.center,
               textAlign: TextAlign.center,
             ),
             ),
@@ -387,6 +393,7 @@ class LoginForm extends HookConsumerWidget {
                     children: [
                     children: [
                       GestureDetector(
                       GestureDetector(
                         onDoubleTap: () => populateTestLoginInfo(),
                         onDoubleTap: () => populateTestLoginInfo(),
+                        onLongPress: () => populateTestLoginInfo1(),
                         child: RotationTransition(
                         child: RotationTransition(
                           turns: logoAnimationController,
                           turns: logoAnimationController,
                           child: const ImmichLogo(
                           child: const ImmichLogo(

+ 1 - 1
mobile/lib/modules/memories/ui/memory_lane.dart

@@ -17,7 +17,7 @@ class MemoryLane extends HookConsumerWidget {
         .whenData(
         .whenData(
           (memories) => memories != null
           (memories) => memories != null
               ? Container(
               ? Container(
-                  margin: const EdgeInsets.only(top: 10),
+                  margin: const EdgeInsets.only(top: 10, left: 10),
                   height: 200,
                   height: 200,
                   child: ListView.builder(
                   child: ListView.builder(
                     scrollDirection: Axis.horizontal,
                     scrollDirection: Axis.horizontal,

+ 3 - 5
mobile/lib/modules/partner/views/partner_detail_page.dart

@@ -1,11 +1,11 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
 import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 
 
 class PartnerDetailPage extends HookConsumerWidget {
 class PartnerDetailPage extends HookConsumerWidget {
@@ -71,8 +71,8 @@ class PartnerDetailPage extends HookConsumerWidget {
           ),
           ),
         ],
         ],
       ),
       ),
-      body: assets.when(
-        data: (renderList) => renderList.isEmpty
+      body: assets.widgetWhen(
+        onData: (renderList) => renderList.isEmpty
             ? Padding(
             ? Padding(
                 padding: const EdgeInsets.all(16),
                 padding: const EdgeInsets.all(16),
                 child: Text(
                 child: Text(
@@ -84,8 +84,6 @@ class PartnerDetailPage extends HookConsumerWidget {
                 onRefresh: () =>
                 onRefresh: () =>
                     ref.read(assetProvider.notifier).getPartnerAssets(partner),
                     ref.read(assetProvider.notifier).getPartnerAssets(partner),
               ),
               ),
-        error: (e, _) => Text("Error loading partners:\n$e"),
-        loading: () => const Center(child: ImmichLoadingIndicator()),
       ),
       ),
     );
     );
   }
   }

+ 12 - 8
mobile/lib/modules/partner/views/partner_page.dart

@@ -1,6 +1,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
 import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
 import 'package:immich_mobile/modules/partner/services/partner.service.dart';
 import 'package:immich_mobile/modules/partner/services/partner.service.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/models/user.dart';
@@ -34,7 +35,7 @@ class PartnerPage extends HookConsumerWidget {
             children: [
             children: [
               for (User u in users)
               for (User u in users)
                 SimpleDialogOption(
                 SimpleDialogOption(
-                  onPressed: () => Navigator.pop(context, u),
+                  onPressed: () => context.pop(u),
                   child: Row(
                   child: Row(
                     children: [
                     children: [
                       Padding(
                       Padding(
@@ -70,8 +71,7 @@ class PartnerPage extends HookConsumerWidget {
         builder: (BuildContext context) {
         builder: (BuildContext context) {
           return ConfirmDialog(
           return ConfirmDialog(
             title: "partner_page_stop_sharing_title",
             title: "partner_page_stop_sharing_title",
-            content:
-                "partner_page_stop_sharing_content".tr(args: [u.name]),
+            content: "partner_page_stop_sharing_content".tr(args: [u.name]),
             onOk: () => ref.read(partnerServiceProvider).removePartner(u),
             onOk: () => ref.read(partnerServiceProvider).removePartner(u),
           );
           );
         },
         },
@@ -118,6 +118,7 @@ class PartnerPage extends HookConsumerWidget {
             Padding(
             Padding(
               padding: const EdgeInsets.symmetric(horizontal: 16.0),
               padding: const EdgeInsets.symmetric(horizontal: 16.0),
               child: Column(
               child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
                 children: [
                 children: [
                   Padding(
                   Padding(
                     padding: const EdgeInsets.symmetric(vertical: 8),
                     padding: const EdgeInsets.symmetric(vertical: 8),
@@ -126,12 +127,15 @@ class PartnerPage extends HookConsumerWidget {
                       style: TextStyle(fontSize: 14),
                       style: TextStyle(fontSize: 14),
                     ).tr(),
                     ).tr(),
                   ),
                   ),
-                  ElevatedButton.icon(
-                    onPressed: availableUsers.whenOrNull(
-                      data: (data) => addNewUsersHandler,
+                  Align(
+                    alignment: Alignment.center,
+                    child: ElevatedButton.icon(
+                      onPressed: availableUsers.whenOrNull(
+                        data: (data) => addNewUsersHandler,
+                      ),
+                      icon: const Icon(Icons.person_add),
+                      label: const Text("partner_page_add_partner").tr(),
                     ),
                     ),
-                    icon: const Icon(Icons.person_add),
-                    label: const Text("partner_page_add_partner").tr(),
                   ),
                   ),
                 ],
                 ],
               ),
               ),

+ 3 - 7
mobile/lib/modules/search/views/all_motion_videos_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart';
 import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 
 class AllMotionPhotosPage extends HookConsumerWidget {
 class AllMotionPhotosPage extends HookConsumerWidget {
   const AllMotionPhotosPage({super.key});
   const AllMotionPhotosPage({super.key});
@@ -21,14 +21,10 @@ class AllMotionPhotosPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         ),
       ),
       ),
-      body: motionPhotos.when(
-        data: (assets) => ImmichAssetGrid(
+      body: motionPhotos.widgetWhen(
+        onData: (assets) => ImmichAssetGrid(
           assets: assets,
           assets: assets,
         ),
         ),
-        error: (e, s) => Text(e.toString()),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
       ),
     );
     );
   }
   }

+ 3 - 7
mobile/lib/modules/search/views/all_people_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 
 class AllPeoplePage extends HookConsumerWidget {
 class AllPeoplePage extends HookConsumerWidget {
   const AllPeoplePage({super.key});
   const AllPeoplePage({super.key});
@@ -23,12 +23,8 @@ class AllPeoplePage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         ),
       ),
       ),
-      body: curatedPeople.when(
-        loading: () => const Center(child: ImmichLoadingIndicator()),
-        error: (err, stack) => Center(
-          child: Text('Error: $err'),
-        ),
-        data: (people) => ExploreGrid(
+      body: curatedPeople.widgetWhen(
+        onData: (people) => ExploreGrid(
           isPeople: true,
           isPeople: true,
           curatedContent: people,
           curatedContent: people,
         ),
         ),

+ 3 - 7
mobile/lib/modules/search/views/all_videos_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart';
 import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 
 class AllVideosPage extends HookConsumerWidget {
 class AllVideosPage extends HookConsumerWidget {
   const AllVideosPage({super.key});
   const AllVideosPage({super.key});
@@ -21,14 +21,10 @@ class AllVideosPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         ),
       ),
       ),
-      body: videos.when(
-        data: (assets) => ImmichAssetGrid(
+      body: videos.widgetWhen(
+        onData: (assets) => ImmichAssetGrid(
           assets: assets,
           assets: assets,
         ),
         ),
-        error: (e, s) => Text(e.toString()),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
       ),
     );
     );
   }
   }

+ 3 - 7
mobile/lib/modules/search/views/curated_location_page.dart

@@ -1,11 +1,11 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 
 
 class CuratedLocationPage extends HookConsumerWidget {
 class CuratedLocationPage extends HookConsumerWidget {
@@ -26,12 +26,8 @@ class CuratedLocationPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         ),
       ),
       ),
-      body: curatedLocation.when(
-        loading: () => const Center(child: ImmichLoadingIndicator()),
-        error: (err, stack) => Center(
-          child: Text('Error: $err'),
-        ),
-        data: (curatedLocations) => ExploreGrid(
+      body: curatedLocation.widgetWhen(
+        onData: (curatedLocations) => ExploreGrid(
           curatedContent: curatedLocations
           curatedContent: curatedLocations
               .map(
               .map(
                 (l) => CuratedContent(
                 (l) => CuratedContent(

+ 1 - 3
mobile/lib/modules/search/views/person_result_page.dart

@@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
 import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart';
 import 'package:immich_mobile/shared/models/store.dart' as isar_store;
 import 'package:immich_mobile/shared/models/store.dart' as isar_store;
-import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 
 
 class PersonResultPage extends HookConsumerWidget {
 class PersonResultPage extends HookConsumerWidget {
@@ -112,7 +111,7 @@ class PersonResultPage extends HookConsumerWidget {
           ),
           ),
         ],
         ],
       ),
       ),
-      body: ref.watch(personAssetsProvider(personId)).scaffoldBodyWhen(
+      body: ref.watch(personAssetsProvider(personId)).widgetWhen(
             onData: (renderList) => ImmichAssetGrid(
             onData: (renderList) => ImmichAssetGrid(
               renderList: renderList,
               renderList: renderList,
               topWidget: Padding(
               topWidget: Padding(
@@ -137,7 +136,6 @@ class PersonResultPage extends HookConsumerWidget {
                 ),
                 ),
               ),
               ),
             ),
             ),
-            onError: const ScaffoldErrorBody(icon: Icons.person_off_outlined),
           ),
           ),
     );
     );
   }
   }

+ 3 - 7
mobile/lib/modules/search/views/recently_added_page.dart

@@ -1,10 +1,10 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart';
 import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 
 class RecentlyAddedPage extends HookConsumerWidget {
 class RecentlyAddedPage extends HookConsumerWidget {
   const RecentlyAddedPage({super.key});
   const RecentlyAddedPage({super.key});
@@ -21,14 +21,10 @@ class RecentlyAddedPage extends HookConsumerWidget {
           icon: const Icon(Icons.arrow_back_ios_rounded),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         ),
       ),
       ),
-      body: recents.when(
-        data: (searchResponse) => ImmichAssetGrid(
+      body: recents.widgetWhen(
+        onData: (searchResponse) => ImmichAssetGrid(
           assets: searchResponse,
           assets: searchResponse,
         ),
         ),
-        error: (e, s) => Text(e.toString()),
-        loading: () => const Center(
-          child: ImmichLoadingIndicator(),
-        ),
       ),
       ),
     );
     );
   }
   }

+ 8 - 9
mobile/lib/modules/search/views/search_page.dart

@@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
@@ -15,7 +16,7 @@ import 'package:immich_mobile/modules/search/ui/search_row_title.dart';
 import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/shared/ui/scaffold_error_body.dart';
 
 
 // ignore: must_be_immutable
 // ignore: must_be_immutable
 class SearchPage extends HookConsumerWidget {
 class SearchPage extends HookConsumerWidget {
@@ -73,10 +74,9 @@ class SearchPage extends HookConsumerWidget {
     buildPeople() {
     buildPeople() {
       return SizedBox(
       return SizedBox(
         height: imageSize,
         height: imageSize,
-        child: curatedPeople.when(
-          loading: () => const Center(child: ImmichLoadingIndicator()),
-          error: (err, stack) => Center(child: Text('Error: $err')),
-          data: (people) => CuratedPeopleRow(
+        child: curatedPeople.widgetWhen(
+          onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
+          onData: (people) => CuratedPeopleRow(
             content: people.take(12).toList(),
             content: people.take(12).toList(),
             onTap: (content, index) {
             onTap: (content, index) {
               context.autoPush(
               context.autoPush(
@@ -97,10 +97,9 @@ class SearchPage extends HookConsumerWidget {
     buildPlaces() {
     buildPlaces() {
       return SizedBox(
       return SizedBox(
         height: imageSize,
         height: imageSize,
-        child: curatedLocation.when(
-          loading: () => const Center(child: ImmichLoadingIndicator()),
-          error: (err, stack) => Center(child: Text('Error: $err')),
-          data: (locations) => CuratedPlacesRow(
+        child: curatedLocation.widgetWhen(
+          onError: (error, stack) => const ScaffoldErrorBody(withIcon: false),
+          onData: (locations) => CuratedPlacesRow(
             isMapEnabled: isMapEnabled,
             isMapEnabled: isMapEnabled,
             content: locations
             content: locations
                 .map(
                 .map(

+ 2 - 3
mobile/lib/modules/settings/views/settings_page.dart

@@ -1,6 +1,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/settings/ui/advanced_settings/advanced_settings.dart';
 import 'package:immich_mobile/modules/settings/ui/advanced_settings/advanced_settings.dart';
 import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
 import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart';
 import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
 import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart';
@@ -18,9 +19,7 @@ class SettingsPage extends HookConsumerWidget {
         leading: IconButton(
         leading: IconButton(
           iconSize: 20,
           iconSize: 20,
           splashRadius: 24,
           splashRadius: 24,
-          onPressed: () {
-            Navigator.pop(context);
-          },
+          onPressed: () => context.pop(),
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
         ),
         ),
         automaticallyImplyLeading: false,
         automaticallyImplyLeading: false,

+ 16 - 6
mobile/lib/modules/shared_link/ui/shared_link_item.dart

@@ -46,9 +46,11 @@ class SharedLinkItem extends ConsumerWidget {
       } else if (difference.inHours > 0) {
       } else if (difference.inHours > 0) {
         expiresText = "shared_link_expires_hours".plural(difference.inHours);
         expiresText = "shared_link_expires_hours".plural(difference.inHours);
       } else if (difference.inMinutes > 0) {
       } else if (difference.inMinutes > 0) {
-        expiresText = "shared_link_expires_minutes".plural(difference.inMinutes);
+        expiresText =
+            "shared_link_expires_minutes".plural(difference.inMinutes);
       } else if (difference.inSeconds > 0) {
       } else if (difference.inSeconds > 0) {
-        expiresText = "shared_link_expires_seconds".plural(difference.inSeconds);
+        expiresText =
+            "shared_link_expires_seconds".plural(difference.inSeconds);
       }
       }
     }
     }
     return Text(
     return Text(
@@ -85,7 +87,12 @@ class SharedLinkItem extends ConsumerWidget {
       ).then((_) {
       ).then((_) {
         ScaffoldMessenger.of(context).showSnackBar(
         ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(
           SnackBar(
-            content: const Text("shared_link_clipboard_copied_massage").tr(),
+            content: Text(
+              "shared_link_clipboard_copied_massage",
+              style: context.textTheme.bodyLarge?.copyWith(
+                color: context.primaryColor,
+              ),
+            ).tr(),
             duration: const Duration(seconds: 2),
             duration: const Duration(seconds: 2),
           ),
           ),
         );
         );
@@ -162,9 +169,12 @@ class SharedLinkItem extends ConsumerWidget {
     Widget buildBottomInfo() {
     Widget buildBottomInfo() {
       return Row(
       return Row(
         children: [
         children: [
-          if (sharedLink.allowUpload) buildInfoChip("shared_link_info_chip_upload".tr()),
-          if (sharedLink.allowDownload) buildInfoChip("shared_link_info_chip_download".tr()),
-          if (sharedLink.showMetadata) buildInfoChip("shared_link_info_chip_metadata".tr()),
+          if (sharedLink.allowUpload)
+            buildInfoChip("shared_link_info_chip_upload".tr()),
+          if (sharedLink.allowDownload)
+            buildInfoChip("shared_link_info_chip_download".tr()),
+          if (sharedLink.showMetadata)
+            buildInfoChip("shared_link_info_chip_metadata".tr()),
         ],
         ],
       );
       );
     }
     }

+ 6 - 1
mobile/lib/modules/shared_link/views/shared_link_edit_page.dart

@@ -275,7 +275,12 @@ class SharedLinkEditPage extends HookConsumerWidget {
       ).then((_) {
       ).then((_) {
         ScaffoldMessenger.of(context).showSnackBar(
         ScaffoldMessenger.of(context).showSnackBar(
           SnackBar(
           SnackBar(
-            content: const Text("shared_link_clipboard_copied_massage").tr(),
+            content: Text(
+              "shared_link_clipboard_copied_massage",
+              style: context.textTheme.bodyLarge?.copyWith(
+                color: context.primaryColor,
+              ),
+            ).tr(),
             duration: const Duration(seconds: 2),
             duration: const Duration(seconds: 2),
           ),
           ),
         );
         );

+ 8 - 6
mobile/lib/modules/shared_link/views/shared_link_page.dart

@@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
 import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
 import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
 import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
 import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart';
 import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 
 class SharedLinkPage extends HookConsumerWidget {
 class SharedLinkPage extends HookConsumerWidget {
   const SharedLinkPage({Key? key}) : super(key: key);
   const SharedLinkPage({Key? key}) : super(key: key);
@@ -18,7 +18,10 @@ class SharedLinkPage extends HookConsumerWidget {
     useEffect(
     useEffect(
       () {
       () {
         ref.read(sharedLinksStateProvider.notifier).fetchLinks();
         ref.read(sharedLinksStateProvider.notifier).fetchLinks();
-        return () => ref.invalidate(sharedLinksStateProvider);
+        return () {
+          if (!context.mounted) return;
+          ref.invalidate(sharedLinksStateProvider);
+        };
       },
       },
       [],
       [],
     );
     );
@@ -113,11 +116,10 @@ class SharedLinkPage extends HookConsumerWidget {
         centerTitle: false,
         centerTitle: false,
       ),
       ),
       body: SafeArea(
       body: SafeArea(
-        child: sharedLinks.when(
-          data: (links) =>
+        child: sharedLinks.widgetWhen(
+          onError: (error, stackTrace) => buildNoShares(),
+          onData: (links) =>
               links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
               links.isNotEmpty ? buildSharesList(links) : buildNoShares(),
-          error: (error, stackTrace) => buildNoShares(),
-          loading: () => const Center(child: ImmichLoadingIndicator()),
         ),
         ),
       ),
       ),
     );
     );

+ 12 - 20
mobile/lib/modules/trash/views/trash_page.dart

@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
@@ -11,8 +12,8 @@ import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
 import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
+import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 
 
 class TrashPage extends HookConsumerWidget {
 class TrashPage extends HookConsumerWidget {
   const TrashPage({super.key});
   const TrashPage({super.key});
@@ -24,7 +25,7 @@ class TrashPage extends HookConsumerWidget {
         ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
         ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays));
     final selectionEnabledHook = useState(false);
     final selectionEnabledHook = useState(false);
     final selection = useState(<Asset>{});
     final selection = useState(<Asset>{});
-    final processing = useState(false);
+    final processing = useProcessingOverlay();
 
 
     void selectionListener(
     void selectionListener(
       bool multiselect,
       bool multiselect,
@@ -229,18 +230,13 @@ class TrashPage extends HookConsumerWidget {
       );
       );
     }
     }
 
 
-    return trashedAssets.when(
-      loading: () => Scaffold(
-        appBar: buildAppBar("?"),
-        body: const Center(child: CircularProgressIndicator()),
+    return Scaffold(
+      appBar: trashedAssets.maybeWhen(
+        orElse: () => buildAppBar("?"),
+        data: (data) => buildAppBar(data.totalAssets.toString()),
       ),
       ),
-      error: (error, stackTrace) => Scaffold(
-        appBar: buildAppBar("!"),
-        body: Center(child: Text(error.toString())),
-      ),
-      data: (data) => Scaffold(
-        appBar: buildAppBar(data.totalAssets.toString()),
-        body: data.isEmpty
+      body: trashedAssets.widgetWhen(
+        onData: (data) => data.isEmpty
             ? Center(
             ? Center(
                 child: Text('trash_page_no_assets'.tr()),
                 child: Text('trash_page_no_assets'.tr()),
               )
               )
@@ -254,11 +250,9 @@ class TrashPage extends HookConsumerWidget {
                       showMultiSelectIndicator: false,
                       showMultiSelectIndicator: false,
                       showStack: true,
                       showStack: true,
                       topWidget: Padding(
                       topWidget: Padding(
-                        padding: const EdgeInsets.only(
-                          top: 24,
-                          bottom: 24,
-                          left: 12,
-                          right: 12,
+                        padding: const EdgeInsets.symmetric(
+                          horizontal: 12,
+                          vertical: 24,
                         ),
                         ),
                         child: const Text(
                         child: const Text(
                           "trash_page_info",
                           "trash_page_info",
@@ -267,8 +261,6 @@ class TrashPage extends HookConsumerWidget {
                     ),
                     ),
                   ),
                   ),
                   if (selectionEnabledHook.value) buildBottomBar(),
                   if (selectionEnabledHook.value) buildBottomBar(),
-                  if (processing.value)
-                    const Center(child: ImmichLoadingIndicator()),
                 ],
                 ],
               ),
               ),
       ),
       ),

+ 1 - 1
mobile/lib/shared/ui/immich_loading_indicator.dart

@@ -21,7 +21,7 @@ class ImmichLoadingIndicator extends StatelessWidget {
       padding: const EdgeInsets.all(15),
       padding: const EdgeInsets.all(15),
       child: const CircularProgressIndicator(
       child: const CircularProgressIndicator(
         color: Colors.white,
         color: Colors.white,
-        strokeWidth: 2,
+        strokeWidth: 3,
       ),
       ),
     );
     );
   }
   }

+ 14 - 11
mobile/lib/shared/ui/scaffold_error_body.dart

@@ -4,9 +4,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 
 // Error widget to be used in Scaffold when an AsyncError is received
 // Error widget to be used in Scaffold when an AsyncError is received
 class ScaffoldErrorBody extends StatelessWidget {
 class ScaffoldErrorBody extends StatelessWidget {
-  final IconData icon;
+  final bool withIcon;
 
 
-  const ScaffoldErrorBody({this.icon = Icons.error_outline, super.key});
+  const ScaffoldErrorBody({super.key, this.withIcon = true});
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
@@ -14,19 +14,22 @@ class ScaffoldErrorBody extends StatelessWidget {
       crossAxisAlignment: CrossAxisAlignment.center,
       crossAxisAlignment: CrossAxisAlignment.center,
       mainAxisAlignment: MainAxisAlignment.center,
       mainAxisAlignment: MainAxisAlignment.center,
       children: [
       children: [
-        const Text(
+        Text(
           "scaffold_body_error_occured",
           "scaffold_body_error_occured",
-          style:
-              TextStyle(fontSize: 14, fontWeight: FontWeight.bold, height: 3),
+          style: context.textTheme.displayMedium,
           textAlign: TextAlign.center,
           textAlign: TextAlign.center,
         ).tr(),
         ).tr(),
-        Center(
-          child: Icon(
-            icon,
-            size: 100,
-            color: context.themeData.iconTheme.color?.withOpacity(0.5),
+        if (withIcon)
+          Center(
+            child: Padding(
+              padding: const EdgeInsets.only(top: 15),
+              child: Icon(
+                Icons.error_outline,
+                size: 100,
+                color: context.themeData.iconTheme.color?.withOpacity(0.5),
+              ),
+            ),
           ),
           ),
-        ),
       ],
       ],
     );
     );
   }
   }

+ 16 - 2
mobile/lib/shared/views/app_log_detail_page.dart

@@ -39,7 +39,14 @@ class AppLogDetailPage extends HookConsumerWidget {
                     Clipboard.setData(ClipboardData(text: stackTrace))
                     Clipboard.setData(ClipboardData(text: stackTrace))
                         .then((_) {
                         .then((_) {
                       ScaffoldMessenger.of(context).showSnackBar(
                       ScaffoldMessenger.of(context).showSnackBar(
-                        const SnackBar(content: Text("Copied to clipboard")),
+                        SnackBar(
+                          content: Text(
+                            "Copied to clipboard",
+                            style: context.textTheme.bodyLarge?.copyWith(
+                              color: context.primaryColor,
+                            ),
+                          ),
+                        ),
                       );
                       );
                     });
                     });
                   },
                   },
@@ -98,7 +105,14 @@ class AppLogDetailPage extends HookConsumerWidget {
                   onPressed: () {
                   onPressed: () {
                     Clipboard.setData(ClipboardData(text: message)).then((_) {
                     Clipboard.setData(ClipboardData(text: message)).then((_) {
                       ScaffoldMessenger.of(context).showSnackBar(
                       ScaffoldMessenger.of(context).showSnackBar(
-                        const SnackBar(content: Text("Copied to clipboard")),
+                        SnackBar(
+                          content: Text(
+                            "Copied to clipboard",
+                            style: context.textTheme.bodyLarge?.copyWith(
+                              color: context.primaryColor,
+                            ),
+                          ),
+                        ),
                       );
                       );
                     });
                     });
                   },
                   },

+ 52 - 29
mobile/lib/shared/views/immich_loading_overlay.dart

@@ -1,41 +1,64 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 
-class ImmichLoadingOverlay extends StatelessWidget {
-  const ImmichLoadingOverlay({
-    Key? key,
-  }) : super(key: key);
+final _loadingEntry = OverlayEntry(
+  builder: (context) => SizedBox.square(
+    dimension: double.infinity,
+    child: DecoratedBox(
+      decoration:
+          BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
+      child: const Center(child: ImmichLoadingIndicator()),
+    ),
+  ),
+);
+
+ValueNotifier<bool> useProcessingOverlay() {
+  return use(const _LoadingOverlay());
+}
+
+class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
+  const _LoadingOverlay();
 
 
   @override
   @override
-  Widget build(BuildContext context) {
-    return ValueListenableBuilder<bool>(
-      valueListenable:
-          ImmichLoadingOverlayController.appLoader.loaderShowingNotifier,
-      builder: (context, shouldShow, child) {
-        return shouldShow
-            ? const Scaffold(
-                backgroundColor: Colors.black54,
-                body: Center(
-                  child: ImmichLoadingIndicator(),
-                ),
-              )
-            : const SizedBox();
-      },
-    );
-  }
+  _LoadingOverlayState createState() => _LoadingOverlayState();
 }
 }
 
 
-class ImmichLoadingOverlayController {
-  static final ImmichLoadingOverlayController appLoader =
-      ImmichLoadingOverlayController();
-  ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
-  ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
+class _LoadingOverlayState
+    extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
+  late final _isProcessing = ValueNotifier(false)..addListener(_listener);
+  OverlayEntry? overlayEntry;
+
+  void _listener() {
+    setState(() {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        if (_isProcessing.value) {
+          overlayEntry?.remove();
+          overlayEntry = _loadingEntry;
+          Overlay.of(context).insert(_loadingEntry);
+        } else {
+          overlayEntry?.remove();
+          overlayEntry = null;
+        }
+      });
+    });
+  }
 
 
-  void show() {
-    loaderShowingNotifier.value = true;
+  @override
+  ValueNotifier<bool> build(BuildContext context) {
+    return _isProcessing;
   }
   }
 
 
-  void hide() {
-    loaderShowingNotifier.value = false;
+  @override
+  void dispose() {
+    _isProcessing.dispose();
+    super.dispose();
   }
   }
+
+  @override
+  Object? get debugValue => _isProcessing.value;
+
+  @override
+  String get debugLabel => 'useProcessingOverlay<>';
 }
 }

+ 18 - 18
mobile/lib/utils/immich_app_theme.dart

@@ -48,8 +48,8 @@ ThemeData immichLightTheme = ThemeData(
     ),
     ),
     backgroundColor: Colors.white,
     backgroundColor: Colors.white,
   ),
   ),
-  appBarTheme: AppBarTheme(
-    titleTextStyle: const TextStyle(
+  appBarTheme: const AppBarTheme(
+    titleTextStyle: TextStyle(
       fontFamily: 'Overpass',
       fontFamily: 'Overpass',
       color: Colors.indigo,
       color: Colors.indigo,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
@@ -61,7 +61,7 @@ ThemeData immichLightTheme = ThemeData(
     scrolledUnderElevation: 0,
     scrolledUnderElevation: 0,
     centerTitle: true,
     centerTitle: true,
   ),
   ),
-  bottomNavigationBarTheme: BottomNavigationBarThemeData(
+  bottomNavigationBarTheme: const BottomNavigationBarThemeData(
     type: BottomNavigationBarType.fixed,
     type: BottomNavigationBarType.fixed,
     backgroundColor: immichBackgroundColor,
     backgroundColor: immichBackgroundColor,
     selectedItemColor: Colors.indigo,
     selectedItemColor: Colors.indigo,
@@ -69,7 +69,7 @@ ThemeData immichLightTheme = ThemeData(
   cardTheme: const CardTheme(
   cardTheme: const CardTheme(
     surfaceTintColor: Colors.transparent,
     surfaceTintColor: Colors.transparent,
   ),
   ),
-  drawerTheme: DrawerThemeData(
+  drawerTheme: const DrawerThemeData(
     backgroundColor: immichBackgroundColor,
     backgroundColor: immichBackgroundColor,
   ),
   ),
   textTheme: const TextTheme(
   textTheme: const TextTheme(
@@ -162,7 +162,7 @@ ThemeData immichDarkTheme = ThemeData(
   hintColor: Colors.grey[600],
   hintColor: Colors.grey[600],
   fontFamily: 'Overpass',
   fontFamily: 'Overpass',
   snackBarTheme: SnackBarThemeData(
   snackBarTheme: SnackBarThemeData(
-    contentTextStyle: TextStyle(
+    contentTextStyle: const TextStyle(
       fontFamily: 'Overpass',
       fontFamily: 'Overpass',
       color: immichDarkThemePrimaryColor,
       color: immichDarkThemePrimaryColor,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
@@ -174,35 +174,35 @@ ThemeData immichDarkTheme = ThemeData(
       foregroundColor: immichDarkThemePrimaryColor,
       foregroundColor: immichDarkThemePrimaryColor,
     ),
     ),
   ),
   ),
-  appBarTheme: AppBarTheme(
+  appBarTheme: const AppBarTheme(
     titleTextStyle: TextStyle(
     titleTextStyle: TextStyle(
       fontFamily: 'Overpass',
       fontFamily: 'Overpass',
       color: immichDarkThemePrimaryColor,
       color: immichDarkThemePrimaryColor,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
       fontSize: 18,
       fontSize: 18,
     ),
     ),
-    backgroundColor: const Color.fromARGB(255, 32, 33, 35),
+    backgroundColor: Color.fromARGB(255, 32, 33, 35),
     foregroundColor: immichDarkThemePrimaryColor,
     foregroundColor: immichDarkThemePrimaryColor,
     elevation: 0,
     elevation: 0,
     scrolledUnderElevation: 0,
     scrolledUnderElevation: 0,
     centerTitle: true,
     centerTitle: true,
   ),
   ),
-  bottomNavigationBarTheme: BottomNavigationBarThemeData(
+  bottomNavigationBarTheme: const BottomNavigationBarThemeData(
     type: BottomNavigationBarType.fixed,
     type: BottomNavigationBarType.fixed,
-    backgroundColor: const Color.fromARGB(255, 35, 36, 37),
+    backgroundColor: Color.fromARGB(255, 35, 36, 37),
     selectedItemColor: immichDarkThemePrimaryColor,
     selectedItemColor: immichDarkThemePrimaryColor,
   ),
   ),
   drawerTheme: DrawerThemeData(
   drawerTheme: DrawerThemeData(
     backgroundColor: immichDarkBackgroundColor,
     backgroundColor: immichDarkBackgroundColor,
     scrimColor: Colors.white.withOpacity(0.1),
     scrimColor: Colors.white.withOpacity(0.1),
   ),
   ),
-  textTheme: TextTheme(
-    displayLarge: const TextStyle(
+  textTheme: const TextTheme(
+    displayLarge: TextStyle(
       fontSize: 26,
       fontSize: 26,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
       color: Color.fromARGB(255, 255, 255, 255),
       color: Color.fromARGB(255, 255, 255, 255),
     ),
     ),
-    displayMedium: const TextStyle(
+    displayMedium: TextStyle(
       fontSize: 14,
       fontSize: 14,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
       color: Color.fromARGB(255, 255, 255, 255),
       color: Color.fromARGB(255, 255, 255, 255),
@@ -212,15 +212,15 @@ ThemeData immichDarkTheme = ThemeData(
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
       color: immichDarkThemePrimaryColor,
       color: immichDarkThemePrimaryColor,
     ),
     ),
-    titleSmall: const TextStyle(
+    titleSmall: TextStyle(
       fontSize: 16.0,
       fontSize: 16.0,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
     ),
     ),
-    titleMedium: const TextStyle(
+    titleMedium: TextStyle(
       fontSize: 18.0,
       fontSize: 18.0,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
     ),
     ),
-    titleLarge: const TextStyle(
+    titleLarge: TextStyle(
       fontSize: 26.0,
       fontSize: 26.0,
       fontWeight: FontWeight.bold,
       fontWeight: FontWeight.bold,
     ),
     ),
@@ -258,7 +258,7 @@ ThemeData immichDarkTheme = ThemeData(
   dialogTheme: const DialogTheme(
   dialogTheme: const DialogTheme(
     surfaceTintColor: Colors.transparent,
     surfaceTintColor: Colors.transparent,
   ),
   ),
-  inputDecorationTheme: InputDecorationTheme(
+  inputDecorationTheme: const InputDecorationTheme(
     focusedBorder: OutlineInputBorder(
     focusedBorder: OutlineInputBorder(
       borderSide: BorderSide(
       borderSide: BorderSide(
         color: immichDarkThemePrimaryColor,
         color: immichDarkThemePrimaryColor,
@@ -267,12 +267,12 @@ ThemeData immichDarkTheme = ThemeData(
     labelStyle: TextStyle(
     labelStyle: TextStyle(
       color: immichDarkThemePrimaryColor,
       color: immichDarkThemePrimaryColor,
     ),
     ),
-    hintStyle: const TextStyle(
+    hintStyle: TextStyle(
       fontSize: 14.0,
       fontSize: 14.0,
       fontWeight: FontWeight.normal,
       fontWeight: FontWeight.normal,
     ),
     ),
   ),
   ),
-  textSelectionTheme: TextSelectionThemeData(
+  textSelectionTheme: const TextSelectionThemeData(
     cursorColor: immichDarkThemePrimaryColor,
     cursorColor: immichDarkThemePrimaryColor,
   ),
   ),
 );
 );

+ 2 - 2
mobile/lib/utils/url_helper.dart

@@ -3,10 +3,10 @@ import 'package:immich_mobile/shared/models/store.dart';
 String sanitizeUrl(String url) {
 String sanitizeUrl(String url) {
   // Add schema if none is set
   // Add schema if none is set
   final urlWithSchema =
   final urlWithSchema =
-      url.startsWith(RegExp(r"https?://")) ? url : "https://$url";
+      url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url";
 
 
   // Remove trailing slash(es)
   // Remove trailing slash(es)
-  return urlWithSchema.replaceFirst(RegExp(r"/+$"), "");
+  return urlWithSchema.trimRight().replaceFirst(RegExp(r"/+$"), "");
 }
 }
 
 
 String? getServerUrl() {
 String? getServerUrl() {

+ 20 - 8
renovate.json

@@ -1,13 +1,19 @@
 {
 {
   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
   "$schema": "https://docs.renovatebot.com/renovate-schema.json",
-  "extends": ["config:base"],
-  "minimumReleaseAge": "5",
+  "extends": ["config:base", "docker:pinDigests"],
+  "minimumReleaseAge": "5 days",
   "packageRules": [
   "packageRules": [
     {
     {
       "matchFileNames": ["cli/**"],
       "matchFileNames": ["cli/**"],
       "groupName": "@immich/cli",
       "groupName": "@immich/cli",
       "matchUpdateTypes": ["minor", "patch"],
       "matchUpdateTypes": ["minor", "patch"],
-      "schedule": "on monday"
+      "schedule": "on tuesday"
+    },
+    {
+      "matchFileNames": ["docs/**"],
+      "groupName": "docs",
+      "matchUpdateTypes": ["minor", "patch"],
+      "schedule": "on tuesday"
     },
     },
     {
     {
       "matchFileNames": ["mobile/**"],
       "matchFileNames": ["mobile/**"],
@@ -20,33 +26,39 @@
       "groupName": "server",
       "groupName": "server",
       "matchUpdateTypes": ["minor", "patch"],
       "matchUpdateTypes": ["minor", "patch"],
       "excludePackagePrefixes": ["exiftool"],
       "excludePackagePrefixes": ["exiftool"],
-      "schedule": "on wednesday"
+      "schedule": "on tuesday"
     },
     },
     {
     {
       "groupName": "exiftool",
       "groupName": "exiftool",
       "matchPackagePrefixes": ["exiftool"],
       "matchPackagePrefixes": ["exiftool"],
-      "schedule": "on monday"
+      "schedule": "on tuesday"
     },
     },
     {
     {
       "matchFileNames": ["web/**"],
       "matchFileNames": ["web/**"],
       "groupName": "web",
       "groupName": "web",
       "matchUpdateTypes": ["minor", "patch"],
       "matchUpdateTypes": ["minor", "patch"],
-      "schedule": "on thursday"
+      "schedule": "on tuesday"
     },
     },
     {
     {
       "matchFileNames": ["machine-learning/**"],
       "matchFileNames": ["machine-learning/**"],
       "groupName": "machine-learning",
       "groupName": "machine-learning",
       "matchUpdateTypes": ["minor", "patch"],
       "matchUpdateTypes": ["minor", "patch"],
-      "schedule": "on friday"
+      "schedule": "on tuesday"
     },
     },
     {
     {
       "matchFileNames": [".github/**"],
       "matchFileNames": [".github/**"],
       "groupName": "github-actions",
       "groupName": "github-actions",
-      "schedule": "on friday"
+      "schedule": "on tuesday"
     },
     },
     {
     {
       "groupName": "base-image",
       "groupName": "base-image",
       "matchPackagePrefixes": ["ghcr.io/immich-app/base-server"]
       "matchPackagePrefixes": ["ghcr.io/immich-app/base-server"]
+    },
+    {
+      "matchDatasources": ["docker"],
+      "matchPackageNames": ["node"],
+      "versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
+      "versioning": "node"
     }
     }
   ],
   ],
   "ignoreDeps": [
   "ignoreDeps": [

+ 1 - 0
server/.eslintrc.js

@@ -19,6 +19,7 @@ module.exports = {
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
     '@typescript-eslint/no-floating-promises': 'error',
     '@typescript-eslint/no-floating-promises': 'error',
+    curly: 2,
     'prettier/prettier': 0,
     'prettier/prettier': 0,
   },
   },
 };
 };

+ 3 - 3
server/Dockerfile

@@ -1,5 +1,5 @@
 # dev build
 # dev build
-FROM ghcr.io/immich-app/base-server-dev:20231125 as dev
+FROM ghcr.io/immich-app/base-server-dev:20231125@sha256:f33b6eaf384e76ef3705a6e2cc76d276144ad6d3366b82f9b45b07d6a19285e2 as dev
 
 
 WORKDIR /usr/src/app
 WORKDIR /usr/src/app
 COPY server/package.json server/package-lock.json ./
 COPY server/package.json server/package-lock.json ./
@@ -13,7 +13,7 @@ RUN npm run build
 RUN npm prune --omit=dev --omit=optional
 RUN npm prune --omit=dev --omit=optional
 
 
 # web build
 # web build
-FROM node:20.10-alpine3.18 as web
+FROM node:iron-alpine3.18 as web
 
 
 WORKDIR /usr/src/app
 WORKDIR /usr/src/app
 COPY web/package.json web/package-lock.json ./
 COPY web/package.json web/package-lock.json ./
@@ -23,7 +23,7 @@ RUN npm run build
 
 
 
 
 # prod build
 # prod build
-FROM ghcr.io/immich-app/base-server-prod:20231125
+FROM ghcr.io/immich-app/base-server-prod:20231125@sha256:a0e15f5bf87a97a79a399a5adffb5fe5befc18fb212e8341e744d958fe41e32a
 
 
 WORKDIR /usr/src/app
 WORKDIR /usr/src/app
 ENV NODE_ENV=production
 ENV NODE_ENV=production

+ 90 - 90
server/package-lock.json

@@ -3256,16 +3256,16 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz",
-      "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+      "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/regexpp": "^4.5.1",
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "6.12.0",
-        "@typescript-eslint/type-utils": "6.12.0",
-        "@typescript-eslint/utils": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/type-utils": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
         "ignore": "^5.2.4",
@@ -3291,15 +3291,15 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/parser": {
     "node_modules/@typescript-eslint/parser": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz",
-      "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+      "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/scope-manager": "6.12.0",
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/typescript-estree": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       },
       },
       "engines": {
       "engines": {
@@ -3319,13 +3319,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/scope-manager": {
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz",
-      "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0"
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
       },
       },
       "engines": {
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
         "node": "^16.0.0 || >=18.0.0"
@@ -3336,13 +3336,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/type-utils": {
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz",
-      "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+      "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "6.12.0",
-        "@typescript-eslint/utils": "6.12.0",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
         "ts-api-utils": "^1.0.1"
       },
       },
@@ -3363,9 +3363,9 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/types": {
     "node_modules/@typescript-eslint/types": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz",
-      "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
         "node": "^16.0.0 || >=18.0.0"
         "node": "^16.0.0 || >=18.0.0"
@@ -3376,13 +3376,13 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/typescript-estree": {
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz",
-      "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
         "is-glob": "^4.0.3",
@@ -3403,17 +3403,17 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/utils": {
     "node_modules/@typescript-eslint/utils": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz",
-      "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+      "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.12.0",
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/typescript-estree": "6.12.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
         "semver": "^7.5.4"
         "semver": "^7.5.4"
       },
       },
       "engines": {
       "engines": {
@@ -3428,12 +3428,12 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/visitor-keys": {
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz",
-      "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "6.12.0",
+        "@typescript-eslint/types": "6.13.1",
         "eslint-visitor-keys": "^3.4.1"
         "eslint-visitor-keys": "^3.4.1"
       },
       },
       "engines": {
       "engines": {
@@ -4347,9 +4347,9 @@
       }
       }
     },
     },
     "node_modules/bullmq": {
     "node_modules/bullmq": {
-      "version": "4.14.2",
-      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.2.tgz",
-      "integrity": "sha512-lzK4F6H61oH5S3Mg4JP4rnSxpQx00Qq7KQKt1oWjcQarka7TdN50CDsZGXg9z6kzvu26Pd3aiwTxwr4YvcEFgw==",
+      "version": "4.14.4",
+      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz",
+      "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==",
       "dependencies": {
       "dependencies": {
         "cron-parser": "^4.6.0",
         "cron-parser": "^4.6.0",
         "glob": "^8.0.3",
         "glob": "^8.0.3",
@@ -15141,16 +15141,16 @@
       "dev": true
       "dev": true
     },
     },
     "@typescript-eslint/eslint-plugin": {
     "@typescript-eslint/eslint-plugin": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz",
-      "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+      "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "@eslint-community/regexpp": "^4.5.1",
         "@eslint-community/regexpp": "^4.5.1",
-        "@typescript-eslint/scope-manager": "6.12.0",
-        "@typescript-eslint/type-utils": "6.12.0",
-        "@typescript-eslint/utils": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/type-utils": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.2.4",
         "ignore": "^5.2.4",
@@ -15160,54 +15160,54 @@
       }
       }
     },
     },
     "@typescript-eslint/parser": {
     "@typescript-eslint/parser": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz",
-      "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+      "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/scope-manager": "6.12.0",
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/typescript-estree": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       }
       }
     },
     },
     "@typescript-eslint/scope-manager": {
     "@typescript-eslint/scope-manager": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz",
-      "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0"
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
       }
       }
     },
     },
     "@typescript-eslint/type-utils": {
     "@typescript-eslint/type-utils": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz",
-      "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+      "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/typescript-estree": "6.12.0",
-        "@typescript-eslint/utils": "6.12.0",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.0.1"
         "ts-api-utils": "^1.0.1"
       }
       }
     },
     },
     "@typescript-eslint/types": {
     "@typescript-eslint/types": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz",
-      "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
       "dev": true
       "dev": true
     },
     },
     "@typescript-eslint/typescript-estree": {
     "@typescript-eslint/typescript-estree": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz",
-      "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/visitor-keys": "6.12.0",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
         "is-glob": "^4.0.3",
@@ -15216,27 +15216,27 @@
       }
       }
     },
     },
     "@typescript-eslint/utils": {
     "@typescript-eslint/utils": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz",
-      "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+      "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
         "@eslint-community/eslint-utils": "^4.4.0",
         "@eslint-community/eslint-utils": "^4.4.0",
         "@types/json-schema": "^7.0.12",
         "@types/json-schema": "^7.0.12",
         "@types/semver": "^7.5.0",
         "@types/semver": "^7.5.0",
-        "@typescript-eslint/scope-manager": "6.12.0",
-        "@typescript-eslint/types": "6.12.0",
-        "@typescript-eslint/typescript-estree": "6.12.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
         "semver": "^7.5.4"
         "semver": "^7.5.4"
       }
       }
     },
     },
     "@typescript-eslint/visitor-keys": {
     "@typescript-eslint/visitor-keys": {
-      "version": "6.12.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz",
-      "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
       "dev": true,
       "dev": true,
       "requires": {
       "requires": {
-        "@typescript-eslint/types": "6.12.0",
+        "@typescript-eslint/types": "6.13.1",
         "eslint-visitor-keys": "^3.4.1"
         "eslint-visitor-keys": "^3.4.1"
       }
       }
     },
     },
@@ -15956,9 +15956,9 @@
       "optional": true
       "optional": true
     },
     },
     "bullmq": {
     "bullmq": {
-      "version": "4.14.2",
-      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.2.tgz",
-      "integrity": "sha512-lzK4F6H61oH5S3Mg4JP4rnSxpQx00Qq7KQKt1oWjcQarka7TdN50CDsZGXg9z6kzvu26Pd3aiwTxwr4YvcEFgw==",
+      "version": "4.14.4",
+      "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz",
+      "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==",
       "requires": {
       "requires": {
         "cron-parser": "^4.6.0",
         "cron-parser": "^4.6.0",
         "glob": "^8.0.3",
         "glob": "^8.0.3",

+ 3 - 1
server/src/domain/metadata/metadata.service.ts

@@ -252,7 +252,9 @@ export class MetadataService {
 
 
     try {
     try {
       const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude });
       const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude });
-      if (!reverseGeocode) return;
+      if (!reverseGeocode) {
+        return;
+      }
       Object.assign(exifData, reverseGeocode);
       Object.assign(exifData, reverseGeocode);
     } catch (error: Error | any) {
     } catch (error: Error | any) {
       this.logger.warn(
       this.logger.warn(

+ 3 - 1
server/src/infra/repositories/system-metadata.repository.ts

@@ -10,7 +10,9 @@ export class SystemMetadataRepository implements ISystemMetadataRepository {
   ) {}
   ) {}
   async get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null> {
   async get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null> {
     const metadata = await this.repository.findOne({ where: { key } });
     const metadata = await this.repository.findOne({ where: { key } });
-    if (!metadata) return null;
+    if (!metadata) {
+      return null;
+    }
     return metadata.value as SystemMetadata[T];
     return metadata.value as SystemMetadata[T];
   }
   }
 
 

+ 1 - 0
web/.eslintrc.cjs

@@ -35,5 +35,6 @@ module.exports = {
         varsIgnorePattern: '^_$',
         varsIgnorePattern: '^_$',
       },
       },
     ],
     ],
+    curly: 2,
   },
   },
 };
 };

+ 2 - 1
web/Dockerfile

@@ -1,5 +1,6 @@
-FROM node:20.10-alpine3.18
+FROM node:iron-alpine3.18
 
 
+USER node
 WORKDIR /usr/src/app
 WORKDIR /usr/src/app
 COPY --chown=node:node package*.json ./
 COPY --chown=node:node package*.json ./
 RUN npm ci
 RUN npm ci

+ 94 - 131
web/package-lock.json

@@ -36,13 +36,12 @@
         "@sveltejs/kit": "^1.20.4",
         "@sveltejs/kit": "^1.20.4",
         "@testing-library/jest-dom": "^6.0.0",
         "@testing-library/jest-dom": "^6.0.0",
         "@testing-library/svelte": "^4.0.3",
         "@testing-library/svelte": "^4.0.3",
-        "@types/cookie": "^0.5.1",
         "@types/dom-to-image": "^2.6.4",
         "@types/dom-to-image": "^2.6.4",
         "@types/justified-layout": "^4.1.0",
         "@types/justified-layout": "^4.1.0",
         "@types/lodash-es": "^4.17.6",
         "@types/lodash-es": "^4.17.6",
         "@types/luxon": "^3.2.0",
         "@types/luxon": "^3.2.0",
-        "@typescript-eslint/eslint-plugin": "^5.53.0",
-        "@typescript-eslint/parser": "^5.53.0",
+        "@typescript-eslint/eslint-plugin": "^6.0.0",
+        "@typescript-eslint/parser": "^6.0.0",
         "autoprefixer": "^10.4.13",
         "autoprefixer": "^10.4.13",
         "babel-jest": "^29.4.3",
         "babel-jest": "^29.4.3",
         "eslint": "^8.34.0",
         "eslint": "^8.34.0",
@@ -3075,6 +3074,12 @@
         "vite": "^4.0.0"
         "vite": "^4.0.0"
       }
       }
     },
     },
+    "node_modules/@sveltejs/kit/node_modules/@types/cookie": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz",
+      "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==",
+      "dev": true
+    },
     "node_modules/@sveltejs/kit/node_modules/magic-string": {
     "node_modules/@sveltejs/kit/node_modules/magic-string": {
       "version": "0.30.5",
       "version": "0.30.5",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
@@ -3415,12 +3420,6 @@
         "@babel/types": "^7.20.7"
         "@babel/types": "^7.20.7"
       }
       }
     },
     },
-    "node_modules/@types/cookie": {
-      "version": "0.5.4",
-      "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz",
-      "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==",
-      "dev": true
-    },
     "node_modules/@types/dom-to-image": {
     "node_modules/@types/dom-to-image": {
       "version": "2.6.7",
       "version": "2.6.7",
       "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.7.tgz",
       "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.7.tgz",
@@ -3532,9 +3531,9 @@
       }
       }
     },
     },
     "node_modules/@types/json-schema": {
     "node_modules/@types/json-schema": {
-      "version": "7.0.13",
-      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz",
-      "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==",
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/@types/justified-layout": {
     "node_modules/@types/justified-layout": {
@@ -3597,9 +3596,9 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@types/semver": {
     "node_modules/@types/semver": {
-      "version": "7.5.3",
-      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz",
-      "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==",
+      "version": "7.5.6",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
+      "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
       "dev": true
       "dev": true
     },
     },
     "node_modules/@types/stack-utils": {
     "node_modules/@types/stack-utils": {
@@ -3643,32 +3642,33 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
-      "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz",
+      "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@eslint-community/regexpp": "^4.4.0",
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/type-utils": "5.62.0",
-        "@typescript-eslint/utils": "5.62.0",
+        "@eslint-community/regexpp": "^4.5.1",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/type-utils": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "graphemer": "^1.4.0",
         "graphemer": "^1.4.0",
-        "ignore": "^5.2.0",
-        "natural-compare-lite": "^1.4.0",
-        "semver": "^7.3.7",
-        "tsutils": "^3.21.0"
+        "ignore": "^5.2.4",
+        "natural-compare": "^1.4.0",
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "@typescript-eslint/parser": "^5.0.0",
-        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+        "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+        "eslint": "^7.0.0 || ^8.0.0"
       },
       },
       "peerDependenciesMeta": {
       "peerDependenciesMeta": {
         "typescript": {
         "typescript": {
@@ -3710,25 +3710,26 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/parser": {
     "node_modules/@typescript-eslint/parser": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
-      "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz",
+      "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/types": "5.62.0",
-        "@typescript-eslint/typescript-estree": "5.62.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4"
         "debug": "^4.3.4"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+        "eslint": "^7.0.0 || ^8.0.0"
       },
       },
       "peerDependenciesMeta": {
       "peerDependenciesMeta": {
         "typescript": {
         "typescript": {
@@ -3737,16 +3738,16 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/scope-manager": {
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
-      "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz",
+      "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "5.62.0",
-        "@typescript-eslint/visitor-keys": "5.62.0"
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
@@ -3754,25 +3755,25 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/type-utils": {
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
-      "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz",
+      "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "5.62.0",
-        "@typescript-eslint/utils": "5.62.0",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "@typescript-eslint/utils": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
-        "tsutils": "^3.21.0"
+        "ts-api-utils": "^1.0.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "eslint": "*"
+        "eslint": "^7.0.0 || ^8.0.0"
       },
       },
       "peerDependenciesMeta": {
       "peerDependenciesMeta": {
         "typescript": {
         "typescript": {
@@ -3781,12 +3782,12 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/types": {
     "node_modules/@typescript-eslint/types": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
-      "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz",
+      "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==",
       "dev": true,
       "dev": true,
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
@@ -3794,21 +3795,21 @@
       }
       }
     },
     },
     "node_modules/@typescript-eslint/typescript-estree": {
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
-      "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz",
+      "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "5.62.0",
-        "@typescript-eslint/visitor-keys": "5.62.0",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/visitor-keys": "6.13.1",
         "debug": "^4.3.4",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
         "is-glob": "^4.0.3",
-        "semver": "^7.3.7",
-        "tsutils": "^3.21.0"
+        "semver": "^7.5.4",
+        "ts-api-utils": "^1.0.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
@@ -3854,29 +3855,28 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/utils": {
     "node_modules/@typescript-eslint/utils": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
-      "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz",
+      "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@eslint-community/eslint-utils": "^4.2.0",
-        "@types/json-schema": "^7.0.9",
-        "@types/semver": "^7.3.12",
-        "@typescript-eslint/scope-manager": "5.62.0",
-        "@typescript-eslint/types": "5.62.0",
-        "@typescript-eslint/typescript-estree": "5.62.0",
-        "eslint-scope": "^5.1.1",
-        "semver": "^7.3.7"
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "@types/json-schema": "^7.0.12",
+        "@types/semver": "^7.5.0",
+        "@typescript-eslint/scope-manager": "6.13.1",
+        "@typescript-eslint/types": "6.13.1",
+        "@typescript-eslint/typescript-estree": "6.13.1",
+        "semver": "^7.5.4"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
         "url": "https://opencollective.com/typescript-eslint"
       },
       },
       "peerDependencies": {
       "peerDependencies": {
-        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+        "eslint": "^7.0.0 || ^8.0.0"
       }
       }
     },
     },
     "node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
     "node_modules/@typescript-eslint/utils/node_modules/lru-cache": {
@@ -3913,16 +3913,16 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/@typescript-eslint/visitor-keys": {
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "5.62.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
-      "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+      "version": "6.13.1",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz",
+      "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==",
       "dev": true,
       "dev": true,
       "dependencies": {
       "dependencies": {
-        "@typescript-eslint/types": "5.62.0",
-        "eslint-visitor-keys": "^3.3.0"
+        "@typescript-eslint/types": "6.13.1",
+        "eslint-visitor-keys": "^3.4.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+        "node": "^16.0.0 || >=18.0.0"
       },
       },
       "funding": {
       "funding": {
         "type": "opencollective",
         "type": "opencollective",
@@ -5614,19 +5614,6 @@
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
       "dev": true
     },
     },
-    "node_modules/eslint-scope": {
-      "version": "5.1.1",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
-      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
-      "dev": true,
-      "dependencies": {
-        "esrecurse": "^4.3.0",
-        "estraverse": "^4.1.1"
-      },
-      "engines": {
-        "node": ">=8.0.0"
-      }
-    },
     "node_modules/eslint-visitor-keys": {
     "node_modules/eslint-visitor-keys": {
       "version": "3.4.3",
       "version": "3.4.3",
       "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
       "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
@@ -5915,15 +5902,6 @@
         "node": ">=4.0"
         "node": ">=4.0"
       }
       }
     },
     },
-    "node_modules/estraverse": {
-      "version": "4.3.0",
-      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-      "dev": true,
-      "engines": {
-        "node": ">=4.0"
-      }
-    },
     "node_modules/estree-walker": {
     "node_modules/estree-walker": {
       "version": "3.0.3",
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
       "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -9665,12 +9643,6 @@
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
       "dev": true
       "dev": true
     },
     },
-    "node_modules/natural-compare-lite": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
-      "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
-      "dev": true
-    },
     "node_modules/neo-async": {
     "node_modules/neo-async": {
       "version": "2.6.2",
       "version": "2.6.2",
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
       "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -11654,6 +11626,18 @@
         "node": ">=12"
         "node": ">=12"
       }
       }
     },
     },
+    "node_modules/ts-api-utils": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
+      "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
+      "dev": true,
+      "engines": {
+        "node": ">=16.13.0"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.2.0"
+      }
+    },
     "node_modules/ts-interface-checker": {
     "node_modules/ts-interface-checker": {
       "version": "0.1.13",
       "version": "0.1.13",
       "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
       "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@@ -11666,27 +11650,6 @@
       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
       "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
       "dev": true
       "dev": true
     },
     },
-    "node_modules/tsutils": {
-      "version": "3.21.0",
-      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
-      "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
-      "dev": true,
-      "dependencies": {
-        "tslib": "^1.8.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      },
-      "peerDependencies": {
-        "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
-      }
-    },
-    "node_modules/tsutils/node_modules/tslib": {
-      "version": "1.14.1",
-      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
-      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
-      "dev": true
-    },
     "node_modules/type-check": {
     "node_modules/type-check": {
       "version": "0.4.0",
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

+ 2 - 3
web/package.json

@@ -28,13 +28,12 @@
     "@sveltejs/kit": "^1.20.4",
     "@sveltejs/kit": "^1.20.4",
     "@testing-library/jest-dom": "^6.0.0",
     "@testing-library/jest-dom": "^6.0.0",
     "@testing-library/svelte": "^4.0.3",
     "@testing-library/svelte": "^4.0.3",
-    "@types/cookie": "^0.5.1",
     "@types/dom-to-image": "^2.6.4",
     "@types/dom-to-image": "^2.6.4",
     "@types/justified-layout": "^4.1.0",
     "@types/justified-layout": "^4.1.0",
     "@types/lodash-es": "^4.17.6",
     "@types/lodash-es": "^4.17.6",
     "@types/luxon": "^3.2.0",
     "@types/luxon": "^3.2.0",
-    "@typescript-eslint/eslint-plugin": "^5.53.0",
-    "@typescript-eslint/parser": "^5.53.0",
+    "@typescript-eslint/eslint-plugin": "^6.0.0",
+    "@typescript-eslint/parser": "^6.0.0",
     "autoprefixer": "^10.4.13",
     "autoprefixer": "^10.4.13",
     "babel-jest": "^29.4.3",
     "babel-jest": "^29.4.3",
     "eslint": "^8.34.0",
     "eslint": "^8.34.0",

+ 5 - 2
web/src/lib/components/admin-page/restore-dialoge.svelte

@@ -9,8 +9,11 @@
 
 
   const restoreUser = async () => {
   const restoreUser = async () => {
     const restoredUser = await api.userApi.restoreUser({ id: user.id });
     const restoredUser = await api.userApi.restoreUser({ id: user.id });
-    if (restoredUser.data.deletedAt == null) dispatch('user-restore-success');
-    else dispatch('user-restore-fail');
+    if (restoredUser.data.deletedAt == null) {
+      dispatch('user-restore-success');
+    } else {
+      dispatch('user-restore-fail');
+    }
   };
   };
 </script>
 </script>
 
 

+ 2 - 1
web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte

@@ -21,7 +21,8 @@
     return '0'.repeat(zeroLength);
     return '0'.repeat(zeroLength);
   };
   };
 
 
-  $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0);
+  const TiB = 1024 ** 4;
+  $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
 </script>
 </script>
 
 
 <div class="flex flex-col gap-5">
 <div class="flex flex-col gap-5">

+ 12 - 2
web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte

@@ -16,8 +16,18 @@
   let savedConfig: SystemConfigJobDto;
   let savedConfig: SystemConfigJobDto;
   let defaultConfig: SystemConfigJobDto;
   let defaultConfig: SystemConfigJobDto;
 
 
-  const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
-  const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
+  const jobNames = [
+    JobName.ThumbnailGeneration,
+    JobName.MetadataExtraction,
+    JobName.Library,
+    JobName.Sidecar,
+    JobName.ObjectTagging,
+    JobName.ClipEncoding,
+    JobName.RecognizeFaces,
+    JobName.VideoConversion,
+    JobName.StorageTemplateMigration,
+    JobName.Migration,
+  ];
 
 
   async function getConfigs() {
   async function getConfigs() {
     [savedConfig, defaultConfig] = await Promise.all([
     [savedConfig, defaultConfig] = await Promise.all([

+ 2 - 0
web/src/lib/components/assets/thumbnail/image-thumbnail.svelte

@@ -17,12 +17,14 @@
   export let circle = false;
   export let circle = false;
   export let hidden = false;
   export let hidden = false;
   export let border = false;
   export let border = false;
+  export let preload = true;
   let complete = false;
   let complete = false;
 
 
   export let eyeColor: 'black' | 'white' = 'white';
   export let eyeColor: 'black' | 'white' = 'white';
 </script>
 </script>
 
 
 <img
 <img
+  loading={preload ? 'eager' : 'lazy'}
   style:width={widthStyle}
   style:width={widthStyle}
   style:height={heightStyle}
   style:height={heightStyle}
   style:filter={hidden ? 'grayscale(50%)' : 'none'}
   style:filter={hidden ? 'grayscale(50%)' : 'none'}

+ 2 - 0
web/src/lib/components/faces-page/people-card.svelte

@@ -12,6 +12,7 @@
   import Icon from '$lib/components/elements/icon.svelte';
   import Icon from '$lib/components/elements/icon.svelte';
 
 
   export let person: PersonResponseDto;
   export let person: PersonResponseDto;
+  export let preload = false;
 
 
   type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face';
   type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face';
   let dispatch = createEventDispatcher<{
   let dispatch = createEventDispatcher<{
@@ -48,6 +49,7 @@
     <div class="h-48 w-48 rounded-xl brightness-95 filter">
     <div class="h-48 w-48 rounded-xl brightness-95 filter">
       <ImageThumbnail
       <ImageThumbnail
         shadow
         shadow
+        {preload}
         url={api.getPeopleThumbnailUrl(person.id)}
         url={api.getPeopleThumbnailUrl(person.id)}
         altText={person.name}
         altText={person.name}
         title={person.name}
         title={person.name}

+ 1 - 1
web/src/lib/utils/byte-units.ts

@@ -9,7 +9,7 @@
  * @returns size (number) and unit (string)
  * @returns size (number) and unit (string)
  */
  */
 export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, string] {
 export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, string] {
-  const units = ['B', 'KiB', 'MiB', 'GiB'];
+  const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
 
 
   let magnitude = 0;
   let magnitude = 0;
   let remainder = bytes;
   let remainder = bytes;

+ 20 - 6
web/src/lib/utils/thumbnail-util.ts

@@ -7,12 +7,26 @@
 export function getThumbnailSize(assetCount: number, viewWidth: number): number {
 export function getThumbnailSize(assetCount: number, viewWidth: number): number {
   if (assetCount < 6) {
   if (assetCount < 6) {
     return Math.min(320, Math.floor(viewWidth / assetCount - assetCount));
     return Math.min(320, Math.floor(viewWidth / assetCount - assetCount));
-  } else {
-    if (viewWidth > 600) return viewWidth / 7 - 7;
-    else if (viewWidth > 400) return viewWidth / 4 - 6;
-    else if (viewWidth > 300) return viewWidth / 2 - 6;
-    else if (viewWidth > 200) return viewWidth / 2 - 6;
-    else if (viewWidth > 100) return viewWidth / 1 - 6;
+  }
+
+  if (viewWidth > 600) {
+    return viewWidth / 7 - 7;
+  }
+
+  if (viewWidth > 400) {
+    return viewWidth / 4 - 6;
+  }
+
+  if (viewWidth > 300) {
+    return viewWidth / 2 - 6;
+  }
+
+  if (viewWidth > 200) {
+    return viewWidth / 2 - 6;
+  }
+
+  if (viewWidth > 100) {
+    return viewWidth / 1 - 6;
   }
   }
 
 
   return 300;
   return 300;

+ 4 - 2
web/src/routes/(user)/people/+page.svelte

@@ -372,10 +372,11 @@
   {#if countVisiblePeople > 0}
   {#if countVisiblePeople > 0}
     <div class="pl-4">
     <div class="pl-4">
       <div class="flex flex-row flex-wrap gap-1">
       <div class="flex flex-row flex-wrap gap-1">
-        {#each people as person (person.id)}
+        {#each people as person, idx (person.id)}
           {#if !person.isHidden}
           {#if !person.isHidden}
             <PeopleCard
             <PeopleCard
               {person}
               {person}
+              preload={idx < 20}
               on:change-name={() => handleChangeName(person)}
               on:change-name={() => handleChangeName(person)}
               on:set-birth-date={() => handleSetBirthDate(person)}
               on:set-birth-date={() => handleSetBirthDate(person)}
               on:merge-faces={() => handleMergeFaces(person)}
               on:merge-faces={() => handleMergeFaces(person)}
@@ -444,7 +445,7 @@
     bind:showLoadingSpinner
     bind:showLoadingSpinner
     bind:toggleVisibility
     bind:toggleVisibility
   >
   >
-    {#each people as person (person.id)}
+    {#each people as person, idx (person.id)}
       <button
       <button
         class="relative h-36 w-36 md:h-48 md:w-48"
         class="relative h-36 w-36 md:h-48 md:w-48"
         on:click={() => (person.isHidden = !person.isHidden)}
         on:click={() => (person.isHidden = !person.isHidden)}
@@ -452,6 +453,7 @@
         on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
         on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
       >
       >
         <ImageThumbnail
         <ImageThumbnail
+          preload={idx < 20}
           bind:hidden={person.isHidden}
           bind:hidden={person.isHidden}
           shadow
           shadow
           url={api.getPeopleThumbnailUrl(person.id)}
           url={api.getPeopleThumbnailUrl(person.id)}