Browse Source

chore: pull main

shalong-tanwen 1 year ago
parent
commit
780d3b174d
100 changed files with 1089 additions and 785 deletions
  1. 2 2
      .github/workflows/docker-cleanup.yml
  2. 1 82
      .github/workflows/docker.yml
  3. 1 1
      README.md
  4. 145 52
      cli/src/api/open-api/api.ts
  5. 1 1
      cli/src/api/open-api/base.ts
  6. 1 1
      cli/src/api/open-api/common.ts
  7. 1 1
      cli/src/api/open-api/configuration.ts
  8. 1 1
      cli/src/api/open-api/index.ts
  9. 2 0
      docker/docker-compose.dev.yml
  10. 2 0
      docker/docker-compose.prod.yml
  11. 4 2
      docker/docker-compose.yml
  12. 1 1
      docs/docs/developer/database-migrations.md
  13. 4 0
      docs/docs/features/bulk-upload.md
  14. 15 0
      docs/docs/features/facial-recognition.md
  15. BIN
      docs/docs/features/img/facial-recognition-4.png
  16. 1 1
      docs/docs/features/mobile-app.mdx
  17. 45 6
      docs/docs/install/config-file.md
  18. BIN
      docs/docs/partials/img/backup-header.png
  19. BIN
      docs/docs/partials/img/sign-in-phone.jpeg
  20. BIN
      docs/docs/partials/img/storage-template.png
  21. 1 1
      docs/src/pages/index.tsx
  22. BIN
      docs/static/img/immich-screenshots.png
  23. 1 1
      machine-learning/pyproject.toml
  24. 2 2
      mobile/android/fastlane/Fastfile
  25. 3 3
      mobile/android/fastlane/report.xml
  26. 5 1
      mobile/assets/i18n/en-US.json
  27. 3 3
      mobile/ios/Runner.xcodeproj/project.pbxproj
  28. 2 2
      mobile/ios/Runner/Info.plist
  29. 1 1
      mobile/ios/fastlane/Fastfile
  30. 6 6
      mobile/ios/fastlane/report.xml
  31. 54 0
      mobile/lib/extensions/build_context_extensions.dart
  32. 0 21
      mobile/lib/extensions/collection_extensions.dart
  33. 0 0
      mobile/lib/extensions/datetime_extensions.dart
  34. 0 0
      mobile/lib/extensions/flutter_map_extensions.dart
  35. 30 0
      mobile/lib/extensions/string_extensions.dart
  36. 70 65
      mobile/lib/modules/activities/views/activities_page.dart
  37. 15 1
      mobile/lib/modules/album/providers/shared_album.provider.dart
  38. 17 0
      mobile/lib/modules/album/services/album.service.dart
  39. 5 6
      mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
  40. 6 7
      mobile/lib/modules/album/ui/album_action_outlined_button.dart
  41. 7 5
      mobile/lib/modules/album/ui/album_thumbnail_card.dart
  42. 2 2
      mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
  43. 3 2
      mobile/lib/modules/album/ui/album_title_text_field.dart
  44. 44 43
      mobile/lib/modules/album/ui/album_viewer_appbar.dart
  45. 4 4
      mobile/lib/modules/album/ui/album_viewer_editable_title.dart
  46. 31 6
      mobile/lib/modules/album/views/album_options_part.dart
  47. 22 13
      mobile/lib/modules/album/views/album_viewer_page.dart
  48. 2 1
      mobile/lib/modules/album/views/asset_selection_page.dart
  49. 25 25
      mobile/lib/modules/album/views/create_album_page.dart
  50. 16 19
      mobile/lib/modules/album/views/library_page.dart
  51. 5 6
      mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
  52. 10 10
      mobile/lib/modules/album/views/select_user_for_sharing_page.dart
  53. 25 29
      mobile/lib/modules/album/views/sharing_page.dart
  54. 2 2
      mobile/lib/modules/archive/views/archive_page.dart
  55. 2 1
      mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart
  56. 5 4
      mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart
  57. 2 2
      mobile/lib/modules/asset_viewer/ui/description_input.dart
  58. 20 9
      mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart
  59. 2 2
      mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
  60. 10 9
      mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
  61. 6 5
      mobile/lib/modules/asset_viewer/views/video_viewer_page.dart
  62. 7 7
      mobile/lib/modules/backup/ui/album_info_card.dart
  63. 8 9
      mobile/lib/modules/backup/ui/album_info_list_tile.dart
  64. 2 3
      mobile/lib/modules/backup/ui/backup_info_card.dart
  65. 5 5
      mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
  66. 3 2
      mobile/lib/modules/backup/ui/ios_debug_info_tile.dart
  67. 2 2
      mobile/lib/modules/backup/views/album_preview_page.dart
  68. 9 9
      mobile/lib/modules/backup/views/backup_album_selection_page.dart
  69. 11 12
      mobile/lib/modules/backup/views/backup_controller_page.dart
  70. 3 3
      mobile/lib/modules/backup/views/failed_backup_status_page.dart
  71. 2 2
      mobile/lib/modules/favorite/views/favorites_page.dart
  72. 3 2
      mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart
  73. 3 5
      mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
  74. 7 6
      mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
  75. 9 9
      mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
  76. 3 2
      mobile/lib/modules/home/ui/control_bottom_app_bar.dart
  77. 4 5
      mobile/lib/modules/home/views/home_page.dart
  78. 3 2
      mobile/lib/modules/login/ui/change_password_form.dart
  79. 9 15
      mobile/lib/modules/login/ui/login_form.dart
  80. 3 3
      mobile/lib/modules/login/views/login_page.dart
  81. 17 3
      mobile/lib/modules/map/models/map_state.model.dart
  82. 104 4
      mobile/lib/modules/map/providers/map_state.provider.dart
  83. 2 2
      mobile/lib/modules/map/ui/map_page_app_bar.dart
  84. 23 32
      mobile/lib/modules/map/ui/map_page_bottom_sheet.dart
  85. 5 4
      mobile/lib/modules/map/ui/map_settings_dialog.dart
  86. 4 15
      mobile/lib/modules/map/ui/map_thumbnail.dart
  87. 62 72
      mobile/lib/modules/map/views/map_page.dart
  88. 2 2
      mobile/lib/modules/memories/ui/memory_lane.dart
  89. 4 4
      mobile/lib/modules/memories/views/memory_page.dart
  90. 21 26
      mobile/lib/modules/onboarding/views/permission_onboarding_page.dart
  91. 3 3
      mobile/lib/modules/partner/ui/partner_list.dart
  92. 3 1
      mobile/lib/modules/search/ui/curated_people_row.dart
  93. 18 15
      mobile/lib/modules/search/ui/curated_places_row.dart
  94. 3 3
      mobile/lib/modules/search/ui/explore_grid.dart
  95. 6 5
      mobile/lib/modules/search/ui/immich_search_bar.dart
  96. 2 1
      mobile/lib/modules/search/ui/person_name_edit_form.dart
  97. 3 2
      mobile/lib/modules/search/ui/search_row_title.dart
  98. 8 8
      mobile/lib/modules/search/ui/search_suggestion_list.dart
  99. 8 8
      mobile/lib/modules/search/ui/thumbnail_with_info.dart
  100. 2 2
      mobile/lib/modules/search/views/all_motion_videos_page.dart

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

@@ -38,7 +38,7 @@ jobs:
       -
         name: Clean temporary images
         if: "${{ env.TOKEN != '' }}"
-        uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
+        uses: stumpylog/image-cleaner-action/ephemeral@v0.4.0
         with:
           token: "${{ env.TOKEN }}"
           owner: "immich-app"
@@ -70,7 +70,7 @@ jobs:
       -
         name: Clean untagged images
         if: "${{ env.TOKEN != '' }}"
-        uses: stumpylog/image-cleaner-action/untagged@v0.3.0
+        uses: stumpylog/image-cleaner-action/untagged@v0.4.0
         with:
           token: "${{ env.TOKEN }}"
           owner: "immich-app"

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

@@ -33,91 +33,10 @@ jobs:
           - context: "nginx"
             image: "immich-proxy"
             platforms: "linux/amd64,linux/arm64"
-
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v4
-
-      - name: Set up QEMU
-        uses: docker/setup-qemu-action@v3.0.0
-
-      - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v3.0.0
-        # Workaround to fix error:
-        # failed to push: failed to copy: io: read/write on closed pipe
-        # See https://github.com/docker/build-push-action/issues/761
-        with:
-          driver-opts: |
-            image=moby/buildkit:v0.10.6
-
-      - name: Login to Docker Hub
-        # Only push to Docker Hub when making a release
-        if: ${{ github.event_name == 'release' }}
-        uses: docker/login-action@v3
-        with:
-          username: ${{ secrets.DOCKERHUB_USERNAME }}
-          password: ${{ secrets.DOCKERHUB_TOKEN }}
-
-      - name: Login to GitHub Container Registry
-        uses: docker/login-action@v3
-        # Skip when PR from a fork
-        if: ${{ !github.event.pull_request.head.repo.fork }}
-        with:
-          registry: ghcr.io
-          username: ${{ github.repository_owner }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-
-      - name: Generate docker image tags
-        id: metadata
-        uses: docker/metadata-action@v5
-        with:
-          flavor: |
-            # Disable latest tag
-            latest=false
-          images: |
-            name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
-            name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
-          tags: |
-            # Tag with branch name
-            type=ref,event=branch
-            # Tag with pr-number
-            type=ref,event=pr
-            # Tag with git tag on release
-            type=ref,event=tag
-            type=raw,value=release,enable=${{ github.event_name == 'release' }}
-
-      - name: Determine build cache output
-        id: cache-target
-        run: |
-          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
-            # Essentially just ignore the cache output (PR can't write to registry cache)
-            echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
-          else
-            echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
-          fi
-
-      - name: Build and push image
-        uses: docker/build-push-action@v5.0.0
-        with:
-          context: ${{ matrix.context }}
-          platforms: ${{ matrix.platforms }}
-          # Skip pushing when PR from a fork
-          push: ${{ !github.event.pull_request.head.repo.fork }}
-          cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
-          cache-to: ${{ steps.cache-target.outputs.cache-to }}
-          tags: ${{ steps.metadata.outputs.tags }}
-          labels: ${{ steps.metadata.outputs.labels }}
-
-  build_and_push_server_arm_64:
-    runs-on: self-hosted
-    strategy:
-      # Prevent a failure in one image from stopping the other builds
-      fail-fast: false
-      matrix:
-        include:
           - context: "server"
             image: "immich-server"
             platforms: "linux/arm64,linux/amd64"
+
     steps:
       - name: Checkout
         uses: actions/checkout@v4

+ 1 - 1
README.md

@@ -2,7 +2,7 @@
   <br/>  
   <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
   <a href="https://discord.gg/D8JsnBEuKb">
-    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
   </a>
   <br/>  
   <br/>   

+ 145 - 52
cli/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.84.0
+ * The version of the OpenAPI document: 1.85.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -209,43 +209,6 @@ export interface AddUsersDto {
      */
     'sharedUserIds': Array<string>;
 }
-/**
- * 
- * @export
- * @interface AdminSignupResponseDto
- */
-export interface AdminSignupResponseDto {
-    /**
-     * 
-     * @type {string}
-     * @memberof AdminSignupResponseDto
-     */
-    'createdAt': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof AdminSignupResponseDto
-     */
-    'email': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof AdminSignupResponseDto
-     */
-    'firstName': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof AdminSignupResponseDto
-     */
-    'id': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof AdminSignupResponseDto
-     */
-    'lastName': string;
-}
 /**
  * 
  * @export
@@ -2261,6 +2224,20 @@ export interface MapMarkerResponseDto {
      */
     'lon': number;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const MapTheme = {
+    Light: 'light',
+    Dark: 'dark'
+} as const;
+
+export type MapTheme = typeof MapTheme[keyof typeof MapTheme];
+
+
 /**
  * 
  * @export
@@ -2593,6 +2570,20 @@ export interface QueueStatusDto {
      */
     'isPaused': boolean;
 }
+/**
+ * 
+ * @export
+ * @enum {string}
+ */
+
+export const ReactionLevel = {
+    Album: 'album',
+    Asset: 'asset'
+} as const;
+
+export type ReactionLevel = typeof ReactionLevel[keyof typeof ReactionLevel];
+
+
 /**
  * 
  * @export
@@ -2859,12 +2850,6 @@ export interface ServerConfigDto {
      * @memberof ServerConfigDto
      */
     'loginPageMessage': string;
-    /**
-     * 
-     * @type {string}
-     * @memberof ServerConfigDto
-     */
-    'mapTileUrl': string;
     /**
      * 
      * @type {string}
@@ -3732,6 +3717,12 @@ export interface SystemConfigMachineLearningDto {
  * @interface SystemConfigMapDto
  */
 export interface SystemConfigMapDto {
+    /**
+     * 
+     * @type {string}
+     * @memberof SystemConfigMapDto
+     */
+    'darkStyle': string;
     /**
      * 
      * @type {boolean}
@@ -3743,7 +3734,7 @@ export interface SystemConfigMapDto {
      * @type {string}
      * @memberof SystemConfigMapDto
      */
-    'tileUrl': string;
+    'lightStyle': string;
 }
 /**
  * 
@@ -5088,11 +5079,12 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
          * @param {string} albumId 
          * @param {string} [assetId] 
          * @param {ReactionType} [type] 
+         * @param {ReactionLevel} [level] 
          * @param {string} [userId] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        getActivities: async (albumId: string, assetId?: string, type?: ReactionType, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        getActivities: async (albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             // verify required parameter 'albumId' is not null or undefined
             assertParamExists('getActivities', 'albumId', albumId)
             const localVarPath = `/activity`;
@@ -5128,6 +5120,10 @@ export const ActivityApiAxiosParamCreator = function (configuration?: Configurat
                 localVarQueryParameter['type'] = type;
             }
 
+            if (level !== undefined) {
+                localVarQueryParameter['level'] = level;
+            }
+
             if (userId !== undefined) {
                 localVarQueryParameter['userId'] = userId;
             }
@@ -5228,12 +5224,13 @@ export const ActivityApiFp = function(configuration?: Configuration) {
          * @param {string} albumId 
          * @param {string} [assetId] 
          * @param {ReactionType} [type] 
+         * @param {ReactionLevel} [level] 
          * @param {string} [userId] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async getActivities(albumId: string, assetId?: string, type?: ReactionType, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, userId, options);
+        async getActivities(albumId: string, assetId?: string, type?: ReactionType, level?: ReactionLevel, userId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<ActivityResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getActivities(albumId, assetId, type, level, userId, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
         /**
@@ -5282,7 +5279,7 @@ export const ActivityApiFactory = function (configuration?: Configuration, baseP
          * @throws {RequiredError}
          */
         getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<ActivityResponseDto>> {
-            return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(axios, basePath));
+            return localVarFp.getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -5351,6 +5348,13 @@ export interface ActivityApiGetActivitiesRequest {
      */
     readonly type?: ReactionType
 
+    /**
+     * 
+     * @type {ReactionLevel}
+     * @memberof ActivityApiGetActivities
+     */
+    readonly level?: ReactionLevel
+
     /**
      * 
      * @type {string}
@@ -5417,7 +5421,7 @@ export class ActivityApi extends BaseAPI {
      * @memberof ActivityApi
      */
     public getActivities(requestParameters: ActivityApiGetActivitiesRequest, options?: AxiosRequestConfig) {
-        return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
+        return ActivityApiFp(this.configuration).getActivities(requestParameters.albumId, requestParameters.assetId, requestParameters.type, requestParameters.level, requestParameters.userId, options).then((request) => request(this.axios, this.basePath));
     }
 
     /**
@@ -10509,7 +10513,7 @@ export const AuthenticationApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AdminSignupResponseDto>> {
+        async signUpAdmin(signUpDto: SignUpDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<UserResponseDto>> {
             const localVarAxiosArgs = await localVarAxiosParamCreator.signUpAdmin(signUpDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
@@ -10589,7 +10593,7 @@ export const AuthenticationApiFactory = function (configuration?: Configuration,
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise<AdminSignupResponseDto> {
+        signUpAdmin(requestParameters: AuthenticationApiSignUpAdminRequest, options?: AxiosRequestConfig): AxiosPromise<UserResponseDto> {
             return localVarFp.signUpAdmin(requestParameters.signUpDto, options).then((request) => request(axios, basePath));
         },
         /**
@@ -15100,6 +15104,51 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config
 
 
     
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @param {MapTheme} theme 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getMapStyle: async (theme: MapTheme, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'theme' is not null or undefined
+            assertParamExists('getMapStyle', 'theme', theme)
+            const localVarPath = `/system-config/map/style.json`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+            if (theme !== undefined) {
+                localVarQueryParameter['theme'] = theme;
+            }
+
+
+    
             setSearchParams(localVarUrlObj, localVarQueryParameter);
             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -15219,6 +15268,16 @@ export const SystemConfigApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.getConfigDefaults(options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {MapTheme} theme 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async getMapStyle(theme: MapTheme, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.getMapStyle(theme, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -15264,6 +15323,15 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
         getConfigDefaults(options?: AxiosRequestConfig): AxiosPromise<SystemConfigDto> {
             return localVarFp.getConfigDefaults(options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig): AxiosPromise<object> {
+            return localVarFp.getMapStyle(requestParameters.theme, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {*} [options] Override http request option.
@@ -15284,6 +15352,20 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
     };
 };
 
+/**
+ * Request parameters for getMapStyle operation in SystemConfigApi.
+ * @export
+ * @interface SystemConfigApiGetMapStyleRequest
+ */
+export interface SystemConfigApiGetMapStyleRequest {
+    /**
+     * 
+     * @type {MapTheme}
+     * @memberof SystemConfigApiGetMapStyle
+     */
+    readonly theme: MapTheme
+}
+
 /**
  * Request parameters for updateConfig operation in SystemConfigApi.
  * @export
@@ -15325,6 +15407,17 @@ export class SystemConfigApi extends BaseAPI {
         return SystemConfigApiFp(this.configuration).getConfigDefaults(options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {SystemConfigApiGetMapStyleRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SystemConfigApi
+     */
+    public getMapStyle(requestParameters: SystemConfigApiGetMapStyleRequest, options?: AxiosRequestConfig) {
+        return SystemConfigApiFp(this.configuration).getMapStyle(requestParameters.theme, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {*} [options] Override http request option.

+ 1 - 1
cli/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.84.0
+ * The version of the OpenAPI document: 1.85.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
cli/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.84.0
+ * The version of the OpenAPI document: 1.85.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
cli/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.84.0
+ * The version of the OpenAPI document: 1.85.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
cli/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.84.0
+ * The version of the OpenAPI document: 1.85.0
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 2 - 0
docker/docker-compose.dev.yml

@@ -4,6 +4,8 @@
 
 version: "3.8"
 
+name: immich-dev
+
 services:
   immich-server:
     container_name: immich_server

+ 2 - 0
docker/docker-compose.prod.yml

@@ -1,5 +1,7 @@
 version: "3.8"
 
+name: immich-prod
+
 services:
   immich-server:
     container_name: immich_server

+ 4 - 2
docker/docker-compose.yml

@@ -1,10 +1,12 @@
 version: "3.8"
 
+name: immich
+
 services:
   immich-server:
     container_name: immich_server
     image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
-    command: ["start.sh", "immich"]
+    command: [ "start.sh", "immich" ]
     volumes:
       - ${UPLOAD_LOCATION}:/usr/src/app/upload
       - /etc/localtime:/etc/localtime:ro
@@ -22,7 +24,7 @@ services:
     # extends:
     #   file: hwaccel.yml
     #   service: hwaccel
-    command: ["start.sh", "microservices"]
+    command: [ "start.sh", "microservices" ]
     volumes:
       - ${UPLOAD_LOCATION}:/usr/src/app/upload
       - /etc/localtime:/etc/localtime:ro

+ 1 - 1
docs/docs/developer/database-migrations.md

@@ -9,6 +9,6 @@ npm run typeorm:migrations:generate ./src/infra/<migration-name>
 ```
 
 2. Check if the migration file makes sense.
-3. Move the migration file to folder `./src/infra/database/migrations` in your code editor.
+3. Move the migration file to folder `./server/src/infra/migrations` in your code editor.
 
 The server will automatically detect `*.ts` file changes and restart. Part of the server start-up process includes running any new migrations, so it will be applied immediately.

+ 4 - 0
docs/docs/features/bulk-upload.md

@@ -4,6 +4,10 @@ You can use the CLI to upload an existing gallery to the Immich server
 
 [Immich CLI Repository](https://github.com/immich-app/CLI)
 
+:::tip Google Photos Takeout
+If you are looking to import your Google Photos takeout, we recommed this community maintained tool [immich-go](https://github.com/simulot/immich-go)
+:::
+
 ## Requirements
 
 - Node.js 16 or above

+ 15 - 0
docs/docs/features/facial-recognition.md

@@ -1,5 +1,7 @@
 # Facial Recognition
 
+## Overview
+
 Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
 
 The list of people is shown in the Explore page.
@@ -13,3 +15,16 @@ Upon clicking on a person, a list of assets that contain their face will be show
 The asset detail view will also show the faces that are recognized in the asset.
 
 <img src={require('./img/facial-recognition-3.png').default} title='Facial Recognition 3' />
+
+## Actions
+
+Additional actions you can do with a detected person are:
+
+- Change the feature face photo of the person
+- Set date of birth
+- Merge two or more detected faces into one person
+- Hide face
+
+It can be found from the app bar when you access the detial view of a person
+
+<img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/>

BIN
docs/docs/features/img/facial-recognition-4.png


+ 1 - 1
docs/docs/features/mobile-app.mdx

@@ -1,6 +1,6 @@
 import MobileAppDownload from '../partials/_mobile-app-download.md';
 import MobileAppLogin from '../partials/_mobile-app-login.md';
-import MobileAppBackup from '../partials/_mobile-app-login.md';
+import MobileAppBackup from '../partials/_mobile-app-backup.md';
 
 # Mobile App
 

+ 45 - 6
docs/docs/install/config-file.md

@@ -17,6 +17,12 @@ The default configuration looks like this:
     "targetAudioCodec": "aac",
     "targetResolution": "720",
     "maxBitrate": "0",
+    "bframes": -1,
+    "refs": 0,
+    "gopSize": 0,
+    "npl": 0,
+    "temporalAQ": false,
+    "cqMode": "auto",
     "twoPass": false,
     "transcode": "required",
     "tonemap": "hable",
@@ -44,9 +50,15 @@ The default configuration looks like this:
     "sidecar": {
       "concurrency": 5
     },
+    "library": {
+      "concurrency": 5
+    },
     "storageTemplateMigration": {
       "concurrency": 5
     },
+    "migration": {
+      "concurrency": 5
+    },
     "thumbnailGeneration": {
       "concurrency": 5
     },
@@ -55,16 +67,16 @@ The default configuration looks like this:
     }
   },
   "machineLearning": {
+    "enabled": true,
+    "url": "http://immich-machine-learning:3003",
     "classification": {
-      "minScore": 0.7,
       "enabled": true,
-      "modelName": "microsoft/resnet-50"
+      "modelName": "microsoft/resnet-50",
+      "minScore": 0.9
     },
-    "enabled": true,
-    "url": "http://immich-machine-learning:3003",
     "clip": {
       "enabled": true,
-      "modelName": "ViT-B-32::openai"
+      "modelName": "ViT-B-32__openai"
     },
     "facialRecognition": {
       "enabled": true,
@@ -74,6 +86,14 @@ The default configuration looks like this:
       "minFaces": 1
     }
   },
+  "map": {
+    "enabled": true,
+    "tileUrl": "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
+  },
+  "reverseGeocoding": {
+    "enabled": true,
+    "citiesFileOverride": "cities500"
+  },
   "oauth": {
     "enabled": false,
     "issuerUrl": "",
@@ -96,8 +116,27 @@ The default configuration looks like this:
   "thumbnail": {
     "webpSize": 250,
     "jpegSize": 1440,
-    "quality": 90,
+    "quality": 80,
     "colorspace": "p3"
+  },
+  "newVersionCheck": {
+    "enabled": true
+  },
+  "trash": {
+    "enabled": true,
+    "days": 30
+  },
+  "theme": {
+    "customCss": ""
+  },
+  "library": {
+    "scan": {
+      "enabled": true,
+      "cronExpression": "0 0 * * *"
+    }
+  },
+  "stylesheets": {
+    "css": ""
   }
 }
 ```

BIN
docs/docs/partials/img/backup-header.png


BIN
docs/docs/partials/img/sign-in-phone.jpeg


BIN
docs/docs/partials/img/storage-template.png


+ 1 - 1
docs/src/pages/index.tsx

@@ -34,7 +34,7 @@ function HomepageHeader() {
           </Link>
         </div>
 
-        <img src="/img/immich-screenshots.png" alt="logo" />
+        <img src="/img/immich-screenshots.png" alt="screenshots" width={'85%'} />
 
         <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
           <div className="h-24">

BIN
docs/static/img/immich-screenshots.png


+ 1 - 1
machine-learning/pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "machine-learning"
-version = "1.84.0"
+version = "1.85.0"
 description = ""
 authors = ["Hau Tran <alex.tran1502@gmail.com>"]
 readme = "README.md"

+ 2 - 2
mobile/android/fastlane/Fastfile

@@ -35,8 +35,8 @@ platform :android do
       task: 'bundle', 
       build_type: 'Release',
       properties: {
-        "android.injected.version.code" => 108,
-        "android.injected.version.name" => "1.84.0",
+        "android.injected.version.code" => 109,
+        "android.injected.version.name" => "1.85.0",
       }
     )
     upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

+ 3 - 3
mobile/android/fastlane/report.xml

@@ -5,17 +5,17 @@
     
     
       
-      <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000625">
+      <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000244">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="1: bundleRelease" time="70.943413">
+      <testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.0562">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.374484">
+      <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.087498">
         
       </testcase>
     

+ 5 - 1
mobile/assets/i18n/en-US.json

@@ -316,6 +316,7 @@
   "shared_link_edit_description": "Description",
   "shared_link_edit_description_hint": "Enter the share description",
   "shared_link_edit_password": "Password",
+  "shared_link_edit_expire_after": "Expire after",
   "shared_link_edit_password_hint": "Enter the share password",
   "shared_link_edit_show_meta": "Show metadata",
   "shared_link_edit_submit_button": "Update link",
@@ -376,5 +377,8 @@
   "app_bar_signout_dialog_ok": "Yes",
   "shared_album_activities_input_hint": "Say something",
   "shared_album_activity_remove_title": "Delete Activity",
-  "shared_album_activity_remove_content": "Do you want to delete this activity?"
+  "shared_album_activity_remove_content": "Do you want to delete this activity?",
+  "shared_album_activity_setting_title": "Comments & likes",
+  "shared_album_activity_setting_subtitle": "Let others respond",
+  "shared_album_activities_input_disable": "Comment is disabled"
 }

+ 3 - 3
mobile/ios/Runner.xcodeproj/project.pbxproj

@@ -379,7 +379,7 @@
 				CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 124;
+				CURRENT_PROJECT_VERSION = 125;
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 124;
+				CURRENT_PROJECT_VERSION = 125;
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_IDENTITY = "Apple Development";
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 124;
+				CURRENT_PROJECT_VERSION = 125;
 				DEVELOPMENT_TEAM = 2F67MQ8R79;
 				ENABLE_BITCODE = NO;
 				INFOPLIST_FILE = Runner/Info.plist;

+ 2 - 2
mobile/ios/Runner/Info.plist

@@ -59,11 +59,11 @@
     <key>CFBundlePackageType</key>
     <string>APPL</string>
     <key>CFBundleShortVersionString</key>
-    <string>1.84.0</string>
+    <string>1.85.0</string>
     <key>CFBundleSignature</key>
     <string>????</string>
     <key>CFBundleVersion</key>
-    <string>124</string>
+    <string>125</string>
     <key>FLTEnableImpeller</key>
     <true />
     <key>ITSAppUsesNonExemptEncryption</key>

+ 1 - 1
mobile/ios/fastlane/Fastfile

@@ -19,7 +19,7 @@ platform :ios do
   desc "iOS Beta"
   lane :beta do
     increment_version_number(
-      version_number: "1.84.0"
+      version_number: "1.85.0"
     )
     increment_build_number(
       build_number: latest_testflight_build_number + 1,

+ 6 - 6
mobile/ios/fastlane/report.xml

@@ -5,32 +5,32 @@
     
     
       
-      <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000253">
+      <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000291">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.181977">
+      <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.199372">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="16.12614">
+      <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.104477">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.162663">
+      <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.164465">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="4: build_app" time="145.399278">
+      <testcase classname="fastlane.lanes" name="4: build_app" time="108.828838">
         
       </testcase>
     
       
-      <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="61.317235">
+      <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="60.89387">
         
       </testcase>
     

+ 54 - 0
mobile/lib/extensions/build_context_extensions.dart

@@ -0,0 +1,54 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+
+extension ContextHelper on BuildContext {
+  // Returns the current size from MediaQuery
+  Size get size => MediaQuery.sizeOf(this);
+
+  // Returns the current width from MediaQuery
+  double get width => size.width;
+
+  // Returns the current height from MediaQuery
+  double get height => size.height;
+
+  // Returns true if the app is running on a mobile device (!tablets)
+  bool get isMobile => width < 550;
+
+  // Returns the current ThemeData
+  ThemeData get themeData => Theme.of(this);
+
+  // Returns true if the app is using a dark theme
+  bool get isDarkTheme => themeData.brightness == Brightness.dark;
+
+  // Returns the current Primary color of the Theme
+  Color get primaryColor => themeData.primaryColor;
+
+  // Returns the Scaffold background color of the Theme
+  Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor;
+
+  // Returns the current TextTheme
+  TextTheme get textTheme => themeData.textTheme;
+
+  // Current ColorScheme used
+  ColorScheme get colorScheme => themeData.colorScheme;
+
+  // Pop-out from the current context with optional result
+  void pop<T>([T? result]) => Navigator.of(this).pop(result);
+
+  // Auto-Push new route from the current context
+  Future<T?> autoPush<T extends Object?>(PageRouteInfo<dynamic> route) =>
+      AutoRouter.of(this).push(route);
+
+  // Auto-Push navigate route from the current context
+  Future<dynamic> autoNavigate<T extends Object?>(
+    PageRouteInfo<dynamic> route,
+  ) =>
+      AutoRouter.of(this).navigate(route);
+
+// Auto-Push replace route from the current context
+  Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
+      AutoRouter.of(this).replace(route);
+
+  // Auto-Pop from the current context
+  Future<bool> autoPop<T>([T? result]) => AutoRouter.of(this).pop(result);
+}

+ 0 - 21
mobile/lib/utils/builtin_extensions.dart → mobile/lib/extensions/collection_extensions.dart

@@ -2,27 +2,6 @@ import 'dart:typed_data';
 
 import 'package:collection/collection.dart';
 
-extension DurationExtension on String {
-  Duration? toDuration() {
-    try {
-      final parts = split(':')
-          .map((e) => double.parse(e).toInt())
-          .toList(growable: false);
-      return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
-    } catch (e) {
-      return null;
-    }
-  }
-
-  double toDouble() {
-    return double.parse(this);
-  }
-
-  int toInt() {
-    return int.parse(this);
-  }
-}
-
 extension ListExtension<E> on List<E> {
   List<E> uniqueConsecutive({
     int Function(E a, E b)? compare,

+ 0 - 0
mobile/lib/utils/datetime_extensions.dart → mobile/lib/extensions/datetime_extensions.dart


+ 0 - 0
mobile/lib/utils/flutter_map_extensions.dart → mobile/lib/extensions/flutter_map_extensions.dart


+ 30 - 0
mobile/lib/extensions/string_extensions.dart

@@ -0,0 +1,30 @@
+extension StringExtension on String {
+  String capitalize() {
+    return split(" ")
+        .map(
+          (str) => str.isEmpty ? str : str[0].toUpperCase() + str.substring(1),
+        )
+        .join(" ");
+  }
+}
+
+extension DurationExtension on String {
+  Duration? toDuration() {
+    try {
+      final parts = split(':')
+          .map((e) => double.parse(e).toInt())
+          .toList(growable: false);
+      return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
+    } catch (e) {
+      return null;
+    }
+  }
+
+  double toDouble() {
+    return double.parse(this);
+  }
+
+  int toInt() {
+    return int.parse(this);
+  }
+}

+ 70 - 65
mobile/lib/modules/activities/views/activities_page.dart

@@ -4,13 +4,14 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/activity.provider.dart';
 import 'package:immich_mobile/shared/models/store.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/utils/datetime_extensions.dart';
+import 'package:immich_mobile/extensions/datetime_extensions.dart';
 import 'package:immich_mobile/utils/image_url_builder.dart';
 
 class ActivitiesPage extends HookConsumerWidget {
@@ -19,12 +20,14 @@ class ActivitiesPage extends HookConsumerWidget {
   final bool withAssetThumbs;
   final String appBarTitle;
   final bool isOwner;
+  final bool isReadOnly;
   const ActivitiesPage(
     this.albumId, {
     this.appBarTitle = "",
     this.assetId,
     this.withAssetThumbs = true,
     this.isOwner = false,
+    this.isReadOnly = false,
     super.key,
   });
 
@@ -45,13 +48,10 @@ class ActivitiesPage extends HookConsumerWidget {
       },
       [],
     );
+
     buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) {
-      final textColor = Theme.of(context).brightness == Brightness.dark
-          ? Colors.white
-          : Colors.black;
-      final textStyle = Theme.of(context)
-          .textTheme
-          .bodyMedium
+      final textColor = context.isDarkTheme ? Colors.white : Colors.black;
+      final textStyle = context.textTheme.bodyMedium
           ?.copyWith(color: textColor.withOpacity(0.6));
 
       return Row(
@@ -116,6 +116,7 @@ class ActivitiesPage extends HookConsumerWidget {
         padding: const EdgeInsets.only(bottom: 10),
         child: TextField(
           controller: inputController,
+          enabled: !isReadOnly,
           focusNode: inputFocusNode,
           textInputAction: TextInputAction.send,
           autofocus: false,
@@ -150,7 +151,9 @@ class ActivitiesPage extends HookConsumerWidget {
               ),
             ),
             suffixIconColor: liked ? Colors.red[700] : null,
-            hintText: 'shared_album_activities_input_hint'.tr(),
+            hintText: isReadOnly
+                ? 'shared_album_activities_input_disable'.tr()
+                : 'shared_album_activities_input_hint'.tr(),
             hintStyle: TextStyle(
               fontWeight: FontWeight.normal,
               fontSize: 14,
@@ -240,70 +243,72 @@ class ActivitiesPage extends HookConsumerWidget {
                 a.assetId == assetId,
           );
 
-          return Stack(
-            children: [
-              ListView.builder(
-                controller: listViewScrollController,
-                itemCount: data.length + 1,
-                itemBuilder: (context, index) {
-                  // Vertical gap after the last element
-                  if (index == data.length) {
-                    return const SizedBox(
-                      height: 80,
-                    );
-                  }
+          return SafeArea(
+            child: Stack(
+              children: [
+                ListView.builder(
+                  controller: listViewScrollController,
+                  itemCount: data.length + 1,
+                  itemBuilder: (context, index) {
+                    // Vertical gap after the last element
+                    if (index == data.length) {
+                      return const SizedBox(
+                        height: 80,
+                      );
+                    }
 
-                  final activity = data[index];
-                  final canDelete =
-                      activity.user.id == currentUser?.id || isOwner;
+                    final activity = data[index];
+                    final canDelete =
+                        activity.user.id == currentUser?.id || isOwner;
 
-                  return Padding(
-                    padding: const EdgeInsets.all(5),
-                    child: activity.type == ActivityType.comment
-                        ? getDismissibleWidget(
-                            ListTile(
-                              minVerticalPadding: 15,
-                              leading: UserCircleAvatar(user: activity.user),
-                              title: buildTitleWithTimestamp(
-                                activity,
-                                leftAlign:
-                                    withAssetThumbs && activity.assetId != null,
+                    return Padding(
+                      padding: const EdgeInsets.all(5),
+                      child: activity.type == ActivityType.comment
+                          ? getDismissibleWidget(
+                              ListTile(
+                                minVerticalPadding: 15,
+                                leading: UserCircleAvatar(user: activity.user),
+                                title: buildTitleWithTimestamp(
+                                  activity,
+                                  leftAlign: withAssetThumbs &&
+                                      activity.assetId != null,
+                                ),
+                                titleAlignment: ListTileTitleAlignment.top,
+                                trailing: buildAssetThumbnail(activity),
+                                subtitle: Text(activity.comment!),
                               ),
-                              titleAlignment: ListTileTitleAlignment.top,
-                              trailing: buildAssetThumbnail(activity),
-                              subtitle: Text(activity.comment!),
-                            ),
-                            activity,
-                            canDelete,
-                          )
-                        : getDismissibleWidget(
-                            ListTile(
-                              minVerticalPadding: 15,
-                              leading: Container(
-                                width: 44,
-                                alignment: Alignment.center,
-                                child: Icon(
-                                  Icons.favorite_rounded,
-                                  color: Colors.red[700],
+                              activity,
+                              canDelete,
+                            )
+                          : getDismissibleWidget(
+                              ListTile(
+                                minVerticalPadding: 15,
+                                leading: Container(
+                                  width: 44,
+                                  alignment: Alignment.center,
+                                  child: Icon(
+                                    Icons.favorite_rounded,
+                                    color: Colors.red[700],
+                                  ),
                                 ),
+                                title: buildTitleWithTimestamp(activity),
+                                trailing: buildAssetThumbnail(activity),
                               ),
-                              title: buildTitleWithTimestamp(activity),
-                              trailing: buildAssetThumbnail(activity),
+                              activity,
+                              canDelete,
                             ),
-                            activity,
-                            canDelete,
-                          ),
-                  );
-                },
-              ),
-              Align(
-                alignment: Alignment.bottomCenter,
-                child: Container(
-                  color: Theme.of(context).scaffoldBackgroundColor,
-                  child: buildTextField(liked?.id),
+                    );
+                  },
                 ),
-              ),
-            ],
+                Align(
+                  alignment: Alignment.bottomCenter,
+                  child: Container(
+                    color: context.scaffoldBackgroundColor,
+                    child: buildTextField(liked?.id),
+                  ),
+                ),
+              ],
+            ),
           );
         },
       ),

+ 15 - 1
mobile/lib/modules/album/providers/shared_album.provider.dart

@@ -2,6 +2,7 @@ import 'dart:async';
 
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -10,7 +11,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:isar/isar.dart';
 
 class SharedAlbumNotifier extends StateNotifier<List<Album>> {
-  SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
+  SharedAlbumNotifier(this._albumService, Isar db, this._ref) : super([]) {
     final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
     query.findAll().then((value) => state = value);
     _streamSub = query.watch().listen((data) => state = data);
@@ -18,6 +19,7 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
 
   final AlbumService _albumService;
   late final StreamSubscription<List<Album>> _streamSub;
+  final Ref _ref;
 
   Future<Album?> createSharedAlbum(
     String albumName,
@@ -66,6 +68,17 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
     return result;
   }
 
+  Future<bool> setActivityEnabled(Album album, bool activityEnabled) async {
+    final result =
+        await _albumService.setActivityEnabled(album, activityEnabled);
+
+    if (result) {
+      _ref.invalidate(albumDetailProvider(album.id));
+    }
+
+    return result;
+  }
+
   @override
   void dispose() {
     _streamSub.cancel();
@@ -78,5 +91,6 @@ final sharedAlbumProvider =
   return SharedAlbumNotifier(
     ref.watch(albumServiceProvider),
     ref.watch(dbProvider),
+    ref,
   );
 });

+ 17 - 0
mobile/lib/modules/album/services/album.service.dart

@@ -284,6 +284,23 @@ class AlbumService {
     return false;
   }
 
+  Future<bool> setActivityEnabled(Album album, bool enabled) async {
+    try {
+      final result = await _apiService.albumApi.updateAlbumInfo(
+        album.remoteId!,
+        UpdateAlbumDto(isActivityEnabled: enabled),
+      );
+      if (result != null) {
+        album.activityEnabled = enabled;
+        await _db.writeTxn(() => _db.albums.put(album));
+        return true;
+      }
+    } catch (e) {
+      debugPrint("Error setActivityEnabled  ${e.toString()}");
+    }
+    return false;
+  }
+
   Future<bool> deleteAlbum(Album album) async {
     try {
       final userId = Store.get(StoreKey.currentUser).isarId;

+ 5 - 6
mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart

@@ -1,8 +1,8 @@
 import 'package:easy_localization/easy_localization.dart';
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -95,20 +95,19 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
                     children: [
                       Text(
                         'common_add_to_album'.tr(),
-                        style: Theme.of(context).textTheme.displayMedium,
+                        style: context.textTheme.displayMedium,
                       ),
                       TextButton.icon(
                         icon: Icon(
                           Icons.add,
-                          color: Theme.of(context).primaryColor,
+                          color: context.primaryColor,
                         ),
                         label: Text(
                           'common_create_new_album'.tr(),
-                          style:
-                              TextStyle(color: Theme.of(context).primaryColor),
+                          style: TextStyle(color: context.primaryColor),
                         ),
                         onPressed: () {
-                          AutoRouter.of(context).push(
+                          context.autoPush(
                             CreateAlbumRoute(
                               isSharedAlbum: false,
                               initialAssets: assets,

+ 6 - 7
mobile/lib/modules/album/ui/album_action_outlined_button.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 class AlbumActionOutlinedButton extends StatelessWidget {
   final VoidCallback? onPressed;
@@ -14,8 +15,6 @@ class AlbumActionOutlinedButton extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
-
     return Padding(
       padding: const EdgeInsets.only(right: 8.0),
       child: OutlinedButton.icon(
@@ -26,7 +25,7 @@ class AlbumActionOutlinedButton extends StatelessWidget {
           ),
           side: BorderSide(
             width: 1,
-            color: isDarkTheme
+            color: context.isDarkTheme
                 ? const Color.fromARGB(255, 63, 63, 63)
                 : const Color.fromARGB(255, 206, 206, 206),
           ),
@@ -34,13 +33,13 @@ class AlbumActionOutlinedButton extends StatelessWidget {
         icon: Icon(
           iconData,
           size: 15,
-          color: Theme.of(context).primaryColor,
+          color: context.primaryColor,
         ),
         label: Text(
           labelText,
-          style: Theme.of(context).textTheme.labelSmall?.copyWith(
-                fontWeight: FontWeight.bold,
-              ),
+          style: context.textTheme.labelSmall?.copyWith(
+            fontWeight: FontWeight.bold,
+          ),
         ),
         onPressed: onPressed,
       ),

+ 7 - 5
mobile/lib/modules/album/ui/album_thumbnail_card.dart

@@ -1,5 +1,6 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -22,7 +23,8 @@ class AlbumThumbnailCard extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
+    var isDarkTheme = context.isDarkTheme;
+
     return LayoutBuilder(
       builder: (context, constraints) {
         var cardSize = constraints.maxWidth;
@@ -81,14 +83,14 @@ class AlbumThumbnailCard extends StatelessWidget {
                   style: TextStyle(
                     fontFamily: 'WorkSans',
                     fontSize: 12,
-                    color: isDarkMode ? Colors.white : Colors.black,
+                    color: isDarkTheme ? Colors.white : Colors.black,
                   ),
                 ),
                 if (owner != null) const TextSpan(text: ' · '),
                 if (owner != null)
                   TextSpan(
                     text: owner,
-                    style: Theme.of(context).textTheme.labelSmall,
+                    style: context.textTheme.labelSmall,
                   ),
               ],
             ),
@@ -122,8 +124,8 @@ class AlbumThumbnailCard extends StatelessWidget {
                           album.name,
                           style: TextStyle(
                             fontWeight: FontWeight.bold,
-                            color: isDarkMode
-                                ? Theme.of(context).primaryColor
+                            color: isDarkTheme
+                                ? context.primaryColor
                                 : Colors.black,
                           ),
                         ),

+ 2 - 2
mobile/lib/modules/album/ui/album_thumbnail_listtile.dart

@@ -1,7 +1,7 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/store.dart';
@@ -70,7 +70,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
       behavior: HitTestBehavior.opaque,
       onTap: onTap ??
           () {
-            AutoRouter.of(context).push(AlbumViewerRoute(albumId: album.id));
+            context.autoPush(AlbumViewerRoute(albumId: album.id));
           },
       child: Padding(
         padding: const EdgeInsets.only(bottom: 12.0),

+ 3 - 2
mobile/lib/modules/album/ui/album_title_text_field.dart

@@ -1,6 +1,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
 
 class AlbumTitleTextField extends ConsumerWidget {
@@ -19,7 +20,7 @@ class AlbumTitleTextField extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+    final isDarkTheme = context.isDarkTheme;
 
     return TextField(
       onChanged: (v) {
@@ -55,7 +56,7 @@ class AlbumTitleTextField extends ConsumerWidget {
                 },
                 icon: Icon(
                   Icons.cancel_rounded,
-                  color: Theme.of(context).primaryColor,
+                  color: context.primaryColor,
                 ),
                 splashRadius: 10,
               )

+ 44 - 43
mobile/lib/modules/album/ui/album_viewer_appbar.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
@@ -58,12 +58,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
       if (album.shared) {
         success =
             await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
-        AutoRouter.of(context)
-            .navigate(const TabControllerRoute(children: [SharingRoute()]));
+        context
+            .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
       } else {
         success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
-        AutoRouter.of(context)
-            .navigate(const TabControllerRoute(children: [LibraryRoute()]));
+        context
+            .autoNavigate(const TabControllerRoute(children: [LibraryRoute()]));
       }
       if (!success) {
         ImmichToast.show(
@@ -93,7 +93,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
                 child: Text(
                   'Cancel',
                   style: TextStyle(
-                    color: Theme.of(context).primaryColor,
+                    color: context.primaryColor,
                     fontWeight: FontWeight.bold,
                   ),
                 ),
@@ -107,9 +107,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
                   'Confirm',
                   style: TextStyle(
                     fontWeight: FontWeight.bold,
-                    color: Theme.of(context).brightness == Brightness.light
-                        ? Colors.red
-                        : Colors.red[300],
+                    color: !context.isDarkTheme ? Colors.red : Colors.red[300],
                   ),
                 ),
               ),
@@ -130,8 +128,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
           await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album);
 
       if (isSuccess) {
-        AutoRouter.of(context)
-            .navigate(const TabControllerRoute(children: [SharingRoute()]));
+        context
+            .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
       } else {
         Navigator.pop(context);
         ImmichToast.show(
@@ -190,7 +188,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
                   gravity: ToastGravity.BOTTOM,
                 );
               }
-              Navigator.of(buildContext).pop();
+              context.pop();
             },
           );
           return const ShareDialog();
@@ -216,32 +214,36 @@ class AlbumViewerAppbar extends HookConsumerWidget
             ).tr(),
             onTap: () => onShareAssetsTo(),
           ),
-          album.ownerId == userId ? ListTile(
-            leading: const Icon(Icons.delete_sweep_rounded),
-            title: const Text(
-              'album_viewer_appbar_share_remove',
-              style: TextStyle(fontWeight: FontWeight.bold),
-            ).tr(),
-            onTap: () => onRemoveFromAlbumPressed(),
-          ) : const SizedBox(),
+          album.ownerId == userId
+              ? ListTile(
+                  leading: const Icon(Icons.delete_sweep_rounded),
+                  title: const Text(
+                    'album_viewer_appbar_share_remove',
+                    style: TextStyle(fontWeight: FontWeight.bold),
+                  ).tr(),
+                  onTap: () => onRemoveFromAlbumPressed(),
+                )
+              : const SizedBox(),
         ];
       } else {
         return [
-          album.ownerId == userId ? ListTile(
-            leading: const Icon(Icons.delete_forever_rounded),
-            title: const Text(
-              'album_viewer_appbar_share_delete',
-              style: TextStyle(fontWeight: FontWeight.bold),
-            ).tr(),
-            onTap: () => onDeleteAlbumPressed(),
-          ) : ListTile(
-            leading: const Icon(Icons.person_remove_rounded),
-            title: const Text(
-              'album_viewer_appbar_share_leave',
-              style: TextStyle(fontWeight: FontWeight.bold),
-            ).tr(),
-            onTap: () => onLeaveAlbumPressed(),
-          ),
+          album.ownerId == userId
+              ? ListTile(
+                  leading: const Icon(Icons.delete_forever_rounded),
+                  title: const Text(
+                    'album_viewer_appbar_share_delete',
+                    style: TextStyle(fontWeight: FontWeight.bold),
+                  ).tr(),
+                  onTap: () => onDeleteAlbumPressed(),
+                )
+              : ListTile(
+                  leading: const Icon(Icons.person_remove_rounded),
+                  title: const Text(
+                    'album_viewer_appbar_share_leave',
+                    style: TextStyle(fontWeight: FontWeight.bold),
+                  ).tr(),
+                  onTap: () => onLeaveAlbumPressed(),
+                ),
         ];
       }
     }
@@ -262,8 +264,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ListTile(
           leading: const Icon(Icons.share_rounded),
           onTap: () {
-            AutoRouter.of(context)
-                .push(SharedLinkEditRoute(albumId: album.remoteId));
+            context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
             Navigator.pop(context);
           },
           title: const Text(
@@ -273,8 +274,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ),
         ListTile(
           leading: const Icon(Icons.settings_rounded),
-          onTap: () =>
-              AutoRouter.of(context).navigate(AlbumOptionsRoute(album: album)),
+          onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
           title: const Text(
             "translated_text_options",
             style: TextStyle(fontWeight: FontWeight.bold),
@@ -296,7 +296,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ),
       ];
       showModalBottomSheet(
-        backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+        backgroundColor: context.scaffoldBackgroundColor,
         isScrollControlled: false,
         context: context,
         builder: (context) {
@@ -338,7 +338,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
                   comments.toString(),
                   style: TextStyle(
                     fontWeight: FontWeight.bold,
-                    color: Theme.of(context).primaryColor,
+                    color: context.primaryColor,
                   ),
                 ),
               ),
@@ -377,7 +377,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         );
       } else {
         return IconButton(
-          onPressed: () async => await AutoRouter.of(context).pop(),
+          onPressed: () async => await context.autoPop(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
           splashRadius: 25,
         );
@@ -390,7 +390,8 @@ class AlbumViewerAppbar extends HookConsumerWidget
       title: selected.isNotEmpty ? Text('${selected.length}') : null,
       centerTitle: false,
       actions: [
-        if (album.shared) buildActivitiesButton(),
+        if (album.shared && (album.activityEnabled || comments != 0))
+          buildActivitiesButton(),
         if (album.isRemote)
           IconButton(
             splashRadius: 25,

+ 4 - 4
mobile/lib/modules/album/ui/album_viewer_editable_title.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 
@@ -17,7 +18,6 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final titleTextEditController = useTextEditingController(text: album.name);
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 
     void onFocusModeChange() {
       if (!titleFocusNode.hasFocus && titleTextEditController.text.isEmpty) {
@@ -65,7 +65,7 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
                 },
                 icon: Icon(
                   Icons.cancel_rounded,
-                  color: Theme.of(context).primaryColor,
+                  color: context.primaryColor,
                 ),
                 splashRadius: 10,
               )
@@ -79,14 +79,14 @@ class AlbumViewerEditableTitle extends HookConsumerWidget {
           borderRadius: BorderRadius.circular(10),
         ),
         focusColor: Colors.grey[300],
-        fillColor: isDarkTheme
+        fillColor: context.isDarkTheme
             ? const Color.fromARGB(255, 32, 33, 35)
             : Colors.grey[200],
         filled: titleFocusNode.hasFocus,
         hintText: 'share_add_title'.tr(),
         hintStyle: TextStyle(
           fontSize: 28,
-          color: isDarkTheme ? Colors.grey[300] : Colors.grey[700],
+          color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700],
           fontWeight: FontWeight.bold,
         ),
       ),

+ 31 - 6
mobile/lib/modules/album/views/album_options_part.dart

@@ -1,9 +1,9 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -23,6 +23,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
     final sharedUsers = useState(album.sharedUsers.toList());
     final owner = album.owner.value;
     final userId = ref.watch(authenticationProvider).userId;
+    final activityEnabled = useState(album.activityEnabled);
     final isOwner = owner?.id == userId;
 
     void showErrorMessage() {
@@ -43,8 +44,9 @@ class AlbumOptionsPage extends HookConsumerWidget {
             await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
 
         if (isSuccess) {
-          AutoRouter.of(context)
-              .navigate(const TabControllerRoute(children: [SharingRoute()]));
+          context.autoNavigate(
+            const TabControllerRoute(children: [SharingRoute()]),
+          );
         } else {
           showErrorMessage();
         }
@@ -96,7 +98,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
       }
 
       showModalBottomSheet(
-        backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+        backgroundColor: context.scaffoldBackgroundColor,
         isScrollControlled: false,
         context: context,
         builder: (context) {
@@ -176,7 +178,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
     buildSectionTitle(String text) {
       return Padding(
         padding: const EdgeInsets.all(16.0),
-        child: Text(text, style: Theme.of(context).textTheme.bodySmall),
+        child: Text(text, style: context.textTheme.bodySmall),
       );
     }
 
@@ -185,7 +187,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
         leading: IconButton(
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
           onPressed: () {
-            AutoRouter.of(context).pop(null);
+            context.autoPop(null);
           },
         ),
         centerTitle: true,
@@ -195,6 +197,29 @@ class AlbumOptionsPage extends HookConsumerWidget {
         mainAxisAlignment: MainAxisAlignment.start,
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
+          if (isOwner && album.shared)
+            SwitchListTile.adaptive(
+              value: activityEnabled.value,
+              onChanged: (bool value) async {
+                activityEnabled.value = value;
+                if (await ref
+                    .read(sharedAlbumProvider.notifier)
+                    .setActivityEnabled(album, value)) {
+                  album.activityEnabled = value;
+                }
+              },
+              activeColor: activityEnabled.value
+                  ? context.primaryColor
+                  : context.themeData.disabledColor,
+              dense: true,
+              title: Text(
+                "shared_album_activity_setting_title",
+                style: context.textTheme.labelLarge
+                    ?.copyWith(fontWeight: FontWeight.bold),
+              ).tr(),
+              subtitle:
+                  const Text("shared_album_activity_setting_subtitle").tr(),
+            ),
           buildSectionTitle("PEOPLE"),
           buildOwnerInfo(),
           buildSharedUsersList(),

+ 22 - 13
mobile/lib/modules/album/views/album_viewer_page.dart

@@ -1,10 +1,10 @@
 import 'dart:async';
 
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/album_detail.provider.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
@@ -67,7 +67,7 @@ class AlbumViewerPage extends HookConsumerWidget {
     /// If they exist, add to selected asset state to show they are already selected.
     void onAddPhotosPressed(Album albumInfo) async {
       AssetSelectionPageResult? returnPayload =
-          await AutoRouter.of(context).push<AssetSelectionPageResult?>(
+          await context.autoPush<AssetSelectionPageResult?>(
         AssetSelectionRoute(
           existingAssets: albumInfo.assets,
           canDeselect: false,
@@ -97,8 +97,7 @@ class AlbumViewerPage extends HookConsumerWidget {
     }
 
     void onAddUsersPressed(Album album) async {
-      List<String>? sharedUserIds =
-          await AutoRouter.of(context).push<List<String>?>(
+      List<String>? sharedUserIds = await context.autoPush<List<String>?>(
         SelectAdditionalUserForSharingRoute(album: album),
       );
 
@@ -171,11 +170,19 @@ class AlbumViewerPage extends HookConsumerWidget {
         return const SizedBox();
       }
 
-      final String startDateText = (startDate.year == endDate.year
-              ? DateFormat.MMMd()
-              : DateFormat.yMMMd())
-          .format(startDate);
-      final String endDateText = DateFormat.yMMMd().format(endDate);
+      final String dateRangeText;
+      if (startDate.day == endDate.day &&
+          startDate.month == endDate.month &&
+          startDate.year == endDate.year) {
+        dateRangeText = DateFormat.yMMMd().format(startDate);
+      } else {
+        final String startDateText = (startDate.year == endDate.year
+                ? DateFormat.MMMd()
+                : DateFormat.yMMMd())
+            .format(startDate);
+        final String endDateText = DateFormat.yMMMd().format(endDate);
+        dateRangeText = "$startDateText - $endDateText";
+      }
 
       return Padding(
         padding: EdgeInsets.only(
@@ -183,7 +190,7 @@ class AlbumViewerPage extends HookConsumerWidget {
           bottom: album.shared ? 0.0 : 8.0,
         ),
         child: Text(
-          "$startDateText - $endDateText",
+          dateRangeText,
           style: const TextStyle(
             fontSize: 14,
             fontWeight: FontWeight.bold,
@@ -195,7 +202,7 @@ class AlbumViewerPage extends HookConsumerWidget {
     Widget buildSharedUserIconsRow(Album album) {
       return GestureDetector(
         onTap: () async {
-          await AutoRouter.of(context).push(AlbumOptionsRoute(album: album));
+          await context.autoPush(AlbumOptionsRoute(album: album));
           ref.invalidate(albumDetailProvider(album.id));
         },
         child: SizedBox(
@@ -234,11 +241,12 @@ class AlbumViewerPage extends HookConsumerWidget {
 
     onActivitiesPressed(Album album) {
       if (album.remoteId != null) {
-        AutoRouter.of(context).push(
+        context.autoPush(
           ActivitiesRoute(
             albumId: album.remoteId!,
             appBarTitle: album.name,
             isOwner: userId == album.ownerId,
+            isReadOnly: !album.activityEnabled,
           ),
         );
       }
@@ -279,7 +287,8 @@ class AlbumViewerPage extends HookConsumerWidget {
                 ],
               ),
               isOwner: userId == data.ownerId,
-              sharedAlbumId: data.remoteId,
+              sharedAlbumId:
+                  data.shared && data.activityEnabled ? data.remoteId : null,
             ),
           ),
         ),

+ 2 - 1
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_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/asset_viewer/providers/render_list.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
@@ -78,7 +79,7 @@ class AssetSelectionPage extends HookConsumerWidget {
                 canDeselect ? "share_done" : "share_add",
                 style: TextStyle(
                   fontWeight: FontWeight.bold,
-                  color: Theme.of(context).primaryColor,
+                  color: context.primaryColor,
                 ),
               ).tr(),
             ),

+ 25 - 25
mobile/lib/modules/album/views/create_album_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
@@ -34,11 +34,11 @@ class CreateAlbumPage extends HookConsumerWidget {
     final selectedAssets = useState<Set<Asset>>(
       initialAssets != null ? Set.from(initialAssets!) : const {},
     );
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 
     showSelectUserPage() async {
-      final bool? ok = await AutoRouter.of(context)
-          .push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value));
+      final bool? ok = await context.autoPush<bool?>(
+        SelectUserForSharingRoute(assets: selectedAssets.value),
+      );
       if (ok == true) {
         selectedAssets.value = {};
       }
@@ -58,7 +58,7 @@ class CreateAlbumPage extends HookConsumerWidget {
 
     onSelectPhotosButtonPressed() async {
       AssetSelectionPageResult? selectedAsset =
-          await AutoRouter.of(context).push<AssetSelectionPageResult?>(
+          await context.autoPush<AssetSelectionPageResult?>(
         AssetSelectionRoute(
           existingAssets: selectedAssets.value,
           canDeselect: true,
@@ -94,10 +94,10 @@ class CreateAlbumPage extends HookConsumerWidget {
             padding: const EdgeInsets.only(top: 200, left: 18),
             child: Text(
               'create_shared_album_page_share_add_assets',
-              style: Theme.of(context).textTheme.displayMedium?.copyWith(
-                    fontSize: 12,
-                    fontWeight: FontWeight.normal,
-                  ),
+              style: context.textTheme.displayMedium?.copyWith(
+                fontSize: 12,
+                fontWeight: FontWeight.normal,
+              ),
             ).tr(),
           ),
         );
@@ -117,7 +117,7 @@ class CreateAlbumPage extends HookConsumerWidget {
                 padding:
                     const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
                 side: BorderSide(
-                  color: isDarkTheme
+                  color: context.isDarkTheme
                       ? const Color.fromARGB(255, 63, 63, 63)
                       : const Color.fromARGB(255, 206, 206, 206),
                 ),
@@ -128,16 +128,16 @@ class CreateAlbumPage extends HookConsumerWidget {
               onPressed: onSelectPhotosButtonPressed,
               icon: Icon(
                 Icons.add_rounded,
-                color: Theme.of(context).primaryColor,
+                color: context.primaryColor,
               ),
               label: Padding(
                 padding: const EdgeInsets.only(left: 8.0),
                 child: Text(
                   'create_shared_album_page_share_select_photos',
-                  style: Theme.of(context).textTheme.labelLarge?.copyWith(
-                        fontSize: 16,
-                        fontWeight: FontWeight.bold,
-                      ),
+                  style: context.textTheme.labelLarge?.copyWith(
+                    fontSize: 16,
+                    fontWeight: FontWeight.bold,
+                  ),
                 ).tr(),
               ),
             ),
@@ -206,7 +206,7 @@ class CreateAlbumPage extends HookConsumerWidget {
         selectedAssets.value = {};
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
 
-        AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
+        context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id));
       }
     }
 
@@ -214,19 +214,19 @@ class CreateAlbumPage extends HookConsumerWidget {
       appBar: AppBar(
         elevation: 0,
         centerTitle: false,
-        backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+        backgroundColor: context.scaffoldBackgroundColor,
         leading: IconButton(
           onPressed: () {
             selectedAssets.value = {};
-            AutoRouter.of(context).pop();
+            context.autoPop();
           },
           icon: const Icon(Icons.close_rounded),
         ),
         title: Text(
           'share_create_album',
-          style: Theme.of(context).textTheme.displayMedium?.copyWith(
-                color: Theme.of(context).primaryColor,
-              ),
+          style: context.textTheme.displayMedium?.copyWith(
+            color: context.primaryColor,
+          ),
         ).tr(),
         actions: [
           if (isSharedAlbum)
@@ -239,8 +239,8 @@ class CreateAlbumPage extends HookConsumerWidget {
                 style: TextStyle(
                   fontWeight: FontWeight.bold,
                   color: albumTitleController.text.isEmpty
-                      ? Theme.of(context).disabledColor
-                      : Theme.of(context).primaryColor,
+                      ? context.themeData.disabledColor
+                      : context.primaryColor,
                 ),
               ),
             ),
@@ -254,7 +254,7 @@ class CreateAlbumPage extends HookConsumerWidget {
                 'create_shared_album_page_create'.tr(),
                 style: TextStyle(
                   fontWeight: FontWeight.bold,
-                  color: Theme.of(context).primaryColor,
+                  color: context.primaryColor,
                 ),
               ),
             ),
@@ -265,7 +265,7 @@ class CreateAlbumPage extends HookConsumerWidget {
         child: CustomScrollView(
           slivers: [
             SliverAppBar(
-              backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+              backgroundColor: context.scaffoldBackgroundColor,
               elevation: 5,
               automaticallyImplyLeading: false,
               pinned: true,

+ 16 - 19
mobile/lib/modules/album/views/library_page.dart

@@ -1,9 +1,9 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -95,15 +95,14 @@ class LibraryPage extends HookConsumerWidget {
                     padding: const EdgeInsets.only(right: 12.0),
                     child: Icon(
                       Icons.check,
-                      color: selected
-                          ? Theme.of(context).primaryColor
-                          : Colors.transparent,
+                      color:
+                          selected ? context.primaryColor : Colors.transparent,
                     ),
                   ),
                   Text(
                     option,
                     style: TextStyle(
-                      color: selected ? Theme.of(context).primaryColor : null,
+                      color: selected ? context.primaryColor : null,
                       fontSize: 12.0,
                     ),
                   ),
@@ -121,13 +120,13 @@ class LibraryPage extends HookConsumerWidget {
             Icon(
               Icons.swap_vert_rounded,
               size: 18,
-              color: Theme.of(context).primaryColor,
+              color: context.primaryColor,
             ),
             Text(
               options[selectedAlbumSortOrder.value],
               style: TextStyle(
                 fontWeight: FontWeight.bold,
-                color: Theme.of(context).primaryColor,
+                color: context.primaryColor,
                 fontSize: 12.0,
               ),
             ),
@@ -143,8 +142,7 @@ class LibraryPage extends HookConsumerWidget {
 
           return GestureDetector(
             onTap: () {
-              AutoRouter.of(context)
-                  .push(CreateAlbumRoute(isSharedAlbum: false));
+              (context).autoPush(CreateAlbumRoute(isSharedAlbum: false));
             },
             child: Padding(
               padding: const EdgeInsets.only(bottom: 32),
@@ -159,11 +157,10 @@ class LibraryPage extends HookConsumerWidget {
                       shape: RoundedRectangleBorder(
                         borderRadius: BorderRadius.circular(20.0),
                       ),
-                      child: Center(
+                      child: const Center(
                         child: Icon(
                           Icons.add_rounded,
                           size: 28,
-                          color: Theme.of(context).primaryColor,
                         ),
                       ),
                     ),
@@ -203,9 +200,9 @@ class LibraryPage extends HookConsumerWidget {
               label,
             ),
           ),
-          style: Theme.of(context).elevatedButtonTheme.style?.copyWith(
-                alignment: Alignment.centerLeft,
-              ),
+          style: context.themeData.elevatedButtonTheme.style?.copyWith(
+            alignment: Alignment.centerLeft,
+          ),
           icon: Icon(
             icon,
           ),
@@ -220,7 +217,7 @@ class LibraryPage extends HookConsumerWidget {
     Widget? shareTrashButton() {
       return trashEnabled
           ? InkWell(
-              onTap: () => AutoRouter.of(context).push(const TrashRoute()),
+              onTap: () => context.autoPush(const TrashRoute()),
               borderRadius: BorderRadius.circular(12),
               child: const Icon(
                 Icons.delete_rounded,
@@ -249,12 +246,12 @@ class LibraryPage extends HookConsumerWidget {
                 children: [
                   buildLibraryNavButton(
                       "library_page_favorites".tr(), Icons.favorite_border, () {
-                    AutoRouter.of(context).navigate(const FavoritesRoute());
+                    context.autoNavigate(const FavoritesRoute());
                   }),
                   const SizedBox(width: 12.0),
                   buildLibraryNavButton(
                       "library_page_archive".tr(), Icons.archive_outlined, () {
-                    AutoRouter.of(context).navigate(const ArchiveRoute());
+                    context.autoNavigate(const ArchiveRoute());
                   }),
                 ],
               ),
@@ -297,7 +294,7 @@ class LibraryPage extends HookConsumerWidget {
 
                   return AlbumThumbnailCard(
                     album: sorted[index - 1],
-                    onTap: () => AutoRouter.of(context).push(
+                    onTap: () => context.autoPush(
                       AlbumViewerRoute(
                         albumId: sorted[index - 1].id,
                       ),
@@ -339,7 +336,7 @@ class LibraryPage extends HookConsumerWidget {
                 childCount: local.length,
                 (context, index) => AlbumThumbnailCard(
                   album: local[index],
-                  onTap: () => AutoRouter.of(context).push(
+                  onTap: () => context.autoPush(
                     AlbumViewerRoute(
                       albumId: local[index].id,
                     ),

+ 5 - 6
mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/user.dart';
@@ -22,14 +22,13 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
     final sharedUsersList = useState<Set<User>>({});
 
     addNewUsersHandler() {
-      AutoRouter.of(context)
-          .pop(sharedUsersList.value.map((e) => e.id).toList());
+      context.autoPop(sharedUsersList.value.map((e) => e.id).toList());
     }
 
     buildTileIcon(User user) {
       if (sharedUsersList.value.contains(user)) {
         return CircleAvatar(
-          backgroundColor: Theme.of(context).primaryColor,
+          backgroundColor: context.primaryColor,
           child: const Icon(
             Icons.check_rounded,
             size: 25,
@@ -50,7 +49,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
           Padding(
             padding: const EdgeInsets.symmetric(horizontal: 8.0),
             child: Chip(
-              backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
+              backgroundColor: context.primaryColor.withOpacity(0.15),
               label: Text(
                 user.email,
                 style: const TextStyle(
@@ -124,7 +123,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
         leading: IconButton(
           icon: const Icon(Icons.close_rounded),
           onPressed: () {
-            AutoRouter.of(context).pop(null);
+            context.autoPop(null);
           },
         ),
         actions: [

+ 10 - 10
mobile/lib/modules/album/views/select_user_for_sharing_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
@@ -35,9 +35,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
         await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
         // ref.watch(assetSelectionProvider.notifier).removeAll();
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
-        AutoRouter.of(context).pop(true);
-        AutoRouter.of(context)
-            .navigate(const TabControllerRoute(children: [SharingRoute()]));
+        context.autoPop(true);
+        context
+            .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
       }
 
       ScaffoldMessenger(
@@ -50,7 +50,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
     buildTileIcon(User user) {
       if (sharedUsersList.value.contains(user)) {
         return CircleAvatar(
-          backgroundColor: Theme.of(context).primaryColor,
+          backgroundColor: context.primaryColor,
           child: const Icon(
             Icons.check_rounded,
             size: 25,
@@ -71,7 +71,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
           Padding(
             padding: const EdgeInsets.symmetric(horizontal: 8.0),
             child: Chip(
-              backgroundColor: Theme.of(context).primaryColor.withOpacity(0.15),
+              backgroundColor: context.primaryColor.withOpacity(0.15),
               label: Text(
                 user.email,
                 style: const TextStyle(
@@ -139,20 +139,20 @@ class SelectUserForSharingPage extends HookConsumerWidget {
       appBar: AppBar(
         title: Text(
           'share_invite',
-          style: TextStyle(color: Theme.of(context).primaryColor),
+          style: TextStyle(color: context.primaryColor),
         ).tr(),
         elevation: 0,
         centerTitle: false,
         leading: IconButton(
           icon: const Icon(Icons.close_rounded),
           onPressed: () async {
-            AutoRouter.of(context).pop();
+            context.autoPop();
           },
         ),
         actions: [
           TextButton(
             style: TextButton.styleFrom(
-              foregroundColor: Theme.of(context).primaryColor,
+              foregroundColor: context.primaryColor,
             ),
             onPressed: sharedUsersList.value.isEmpty ? null : createSharedAlbum,
             child: const Text(
@@ -160,7 +160,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
               style: TextStyle(
                 fontSize: 14,
                 fontWeight: FontWeight.bold,
-                // color: Theme.of(context).primaryColor,
+                // color: context.primaryColor,
               ),
             ).tr(),
           ),

+ 25 - 29
mobile/lib/modules/album/views/sharing_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart';
 import 'package:immich_mobile/modules/partner/providers/partner.provider.dart';
@@ -21,7 +21,6 @@ class SharingPage extends HookConsumerWidget {
     final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
     final userId = ref.watch(currentUserProvider)?.id;
     final partner = ref.watch(partnerSharedWithProvider);
-    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
 
     useEffect(
       () {
@@ -47,8 +46,9 @@ class SharingPage extends HookConsumerWidget {
                 album: sharedAlbums[index],
                 showOwner: true,
                 onTap: () {
-                  AutoRouter.of(context)
-                      .push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
+                  context.autoPush(
+                    AlbumViewerRoute(albumId: sharedAlbums[index].id),
+                  );
                 },
               );
             },
@@ -79,12 +79,11 @@ class SharingPage extends HookConsumerWidget {
                 album.name,
                 maxLines: 1,
                 overflow: TextOverflow.ellipsis,
-                style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                      fontWeight: FontWeight.bold,
-                      color: isDarkMode
-                          ? Theme.of(context).primaryColor
-                          : Colors.black,
-                    ),
+                style: context.textTheme.bodyMedium?.copyWith(
+                  fontWeight: FontWeight.bold,
+                  color:
+                      context.isDarkTheme ? context.primaryColor : Colors.black,
+                ),
               ),
               subtitle: isOwner
                   ? Text(
@@ -103,8 +102,9 @@ class SharingPage extends HookConsumerWidget {
                         )
                       : null,
               onTap: () {
-                AutoRouter.of(context)
-                    .push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
+                context.autoPush(
+                  AlbumViewerRoute(albumId: sharedAlbums[index].id),
+                );
               },
             );
           },
@@ -127,16 +127,15 @@ class SharingPage extends HookConsumerWidget {
             Expanded(
               child: ElevatedButton.icon(
                 onPressed: () {
-                  AutoRouter.of(context)
-                      .push(CreateAlbumRoute(isSharedAlbum: true));
+                  context.autoPush(CreateAlbumRoute(isSharedAlbum: true));
                 },
                 icon: const Icon(
                   Icons.photo_album_outlined,
                   size: 20,
                 ),
-                style: Theme.of(context).elevatedButtonTheme.style?.copyWith(
-                      alignment: Alignment.centerLeft,
-                    ),
+                style: context.themeData.elevatedButtonTheme.style?.copyWith(
+                  alignment: Alignment.centerLeft,
+                ),
                 label: const Text(
                   "sharing_silver_appbar_create_shared_album",
                   maxLines: 1,
@@ -146,15 +145,14 @@ class SharingPage extends HookConsumerWidget {
             const SizedBox(width: 12.0),
             Expanded(
               child: ElevatedButton.icon(
-                onPressed: () =>
-                    AutoRouter.of(context).push(const SharedLinkRoute()),
+                onPressed: () => context.autoPush(const SharedLinkRoute()),
                 icon: const Icon(
                   Icons.link,
                   size: 20,
                 ),
-                style: Theme.of(context).elevatedButtonTheme.style?.copyWith(
-                      alignment: Alignment.centerLeft,
-                    ),
+                style: context.themeData.elevatedButtonTheme.style?.copyWith(
+                  alignment: Alignment.centerLeft,
+                ),
                 label: const Text(
                   "sharing_silver_appbar_shared_links",
                   maxLines: 1,
@@ -189,24 +187,22 @@ class SharingPage extends HookConsumerWidget {
                     child: Icon(
                       Icons.insert_photo_rounded,
                       size: 50,
-                      color: Theme.of(context).primaryColor,
+                      color: context.primaryColor,
                     ),
                   ),
                   Padding(
                     padding: const EdgeInsets.all(8.0),
                     child: Text(
                       'sharing_page_empty_list',
-                      style: Theme.of(context)
-                          .textTheme
-                          .displaySmall
-                          ?.copyWith(color: Theme.of(context).primaryColor),
+                      style: context.textTheme.displaySmall
+                          ?.copyWith(color: context.primaryColor),
                     ).tr(),
                   ),
                   Padding(
                     padding: const EdgeInsets.all(8.0),
                     child: Text(
                       'sharing_page_description',
-                      style: Theme.of(context).textTheme.bodyMedium,
+                      style: context.textTheme.bodyMedium,
                     ).tr(),
                   ),
                 ],
@@ -219,7 +215,7 @@ class SharingPage extends HookConsumerWidget {
 
     Widget sharePartnerButton() {
       return InkWell(
-        onTap: () => AutoRouter.of(context).push(const PartnerRoute()),
+        onTap: () => context.autoPush(const PartnerRoute()),
         borderRadius: BorderRadius.circular(12),
         child: const Icon(
           Icons.swap_horizontal_circle_rounded,

+ 2 - 2
mobile/lib/modules/archive/views/archive_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -30,7 +30,7 @@ class ArchivePage extends HookConsumerWidget {
     AppBar buildAppBar(String count) {
       return AppBar(
         leading: IconButton(
-          onPressed: () => AutoRouter.of(context).pop(),
+          onPressed: () => context.autoPop(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         centerTitle: true,

+ 2 - 1
mobile/lib/modules/asset_viewer/providers/image_viewer_page_state.provider.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
 import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 import 'package:immich_mobile/modules/asset_viewer/services/image_viewer.service.dart';
@@ -67,7 +68,7 @@ class ImageViewerStateNotifier extends StateNotifier<ImageViewerPageState> {
                 gravity: ToastGravity.BOTTOM,
               );
             }
-            Navigator.of(buildContext).pop();
+            context.pop();
           },
         );
         return const ShareDialog();

+ 5 - 4
mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 
 class AdvancedBottomSheet extends HookConsumerWidget {
@@ -11,8 +12,6 @@ class AdvancedBottomSheet extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
-
     return SingleChildScrollView(
       child: Card(
         shape: const RoundedRectangleBorder(
@@ -40,7 +39,9 @@ class AdvancedBottomSheet extends HookConsumerWidget {
                   const SizedBox(height: 32.0),
                   Container(
                     decoration: BoxDecoration(
-                      color: isDarkMode ? Colors.grey[900] : Colors.grey[200],
+                      color: context.isDarkTheme
+                          ? Colors.grey[900]
+                          : Colors.grey[200],
                       borderRadius: BorderRadius.circular(15.0),
                     ),
                     child: Padding(
@@ -70,7 +71,7 @@ class AdvancedBottomSheet extends HookConsumerWidget {
                               icon: Icon(
                                 Icons.copy,
                                 size: 16.0,
-                                color: Theme.of(context).primaryColor,
+                                color: context.primaryColor,
                               ),
                             ),
                           ),

+ 2 - 2
mobile/lib/modules/asset_viewer/ui/description_input.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/user.provider.dart';
@@ -19,8 +20,7 @@ class DescriptionInput extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
-    final textColor = isDarkTheme ? Colors.white : Colors.black;
+    final textColor = context.isDarkTheme ? Colors.white : Colors.black;
     final controller = useTextEditingController();
     final focusNode = useFocusNode();
     final isFocus = useState(false);

+ 20 - 9
mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart

@@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_map/flutter_map.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:timezone/timezone.dart';
 import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart';
 import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
@@ -28,7 +29,7 @@ class ExifBottomSheet extends HookConsumerWidget {
       exifInfo.longitude != 0;
 
   String formatTimeZone(Duration d) =>
-      "GMT${d.isNegative ? '-': '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
+      "GMT${d.isNegative ? '-' : '+'}${d.inHours.abs().toString().padLeft(2, '0')}:${d.inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
 
   String get formattedDateTime {
     DateTime dt = asset.fileCreatedAt.toLocal();
@@ -41,10 +42,16 @@ class ExifBottomSheet extends HookConsumerWidget {
           final location = getLocation(asset.exifInfo!.timeZone!);
           dt = TZDateTime.from(dt, location);
         } on LocationNotFoundException {
-          RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false);
+          RegExp re = RegExp(
+            r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
+            caseSensitive: false,
+          );
           final m = re.firstMatch(asset.exifInfo!.timeZone!);
           if (m != null) {
-            final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0'));
+            final duration = Duration(
+              hours: int.parse(m.group(1) ?? '0'),
+              minutes: int.parse(m.group(2) ?? '0'),
+            );
             dt = dt.add(duration);
             timeZone = formatTimeZone(duration);
           }
@@ -105,8 +112,7 @@ class ExifBottomSheet extends HookConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     final assetWithExif = ref.watch(assetDetailProvider(asset));
     final exifInfo = (assetWithExif.value ?? asset).exifInfo;
-    var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
-    var textColor = isDarkTheme ? Colors.white : Colors.black;
+    var textColor = context.isDarkTheme ? Colors.white : Colors.black;
 
     buildMap() {
       return Padding(
@@ -322,9 +328,14 @@ class ExifBottomSheet extends HookConsumerWidget {
                   fontWeight: FontWeight.bold,
                 ),
               ),
-              subtitle: exifInfo.f != null || exifInfo.exposureSeconds != null || exifInfo.mm != null || exifInfo.iso != null ? Text(
-                "ƒ/${exifInfo.fNumber}   ${exifInfo.exposureTime}   ${exifInfo.focalLength} mm   ISO ${exifInfo.iso ?? ''} ",
-              ) : null,
+              subtitle: exifInfo.f != null ||
+                      exifInfo.exposureSeconds != null ||
+                      exifInfo.mm != null ||
+                      exifInfo.iso != null
+                  ? Text(
+                      "ƒ/${exifInfo.fNumber}   ${exifInfo.exposureTime}   ${exifInfo.focalLength} mm   ISO ${exifInfo.iso ?? ''} ",
+                    )
+                  : null,
             ),
         ],
       );
@@ -393,7 +404,7 @@ class ExifBottomSheet extends HookConsumerWidget {
                       data: (data) => DescriptionInput(asset: data),
                       error: (error, stackTrace) => Icon(
                         Icons.image_not_supported_outlined,
-                        color: Theme.of(context).primaryColor,
+                        color: context.primaryColor,
                       ),
                       loading: () => const SizedBox(
                         width: 75,

+ 2 - 2
mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart

@@ -1,6 +1,6 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
@@ -147,7 +147,7 @@ class TopControlAppBar extends HookConsumerWidget {
     Widget buildBackButton() {
       return IconButton(
         onPressed: () {
-          AutoRouter.of(context).pop();
+          context.autoPop();
         },
         icon: Icon(
           Icons.arrow_back_ios_new_rounded,

+ 10 - 9
mobile/lib/modules/asset_viewer/views/gallery_viewer.dart

@@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
@@ -209,7 +210,7 @@ class GalleryViewerPage extends HookConsumerWidget {
         if (isDeleted && isParent) {
           if (totalAssets == 1) {
             // Handle only one asset
-            AutoRouter.of(context).pop();
+            context.autoPop();
           } else {
             // Go to next page otherwise
             controller.nextPage(
@@ -293,7 +294,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 
       final ratio = d.dy / max(d.dx.abs(), 1);
       if (d.dy > sensitivity && ratio > ratioThreshold) {
-        AutoRouter.of(context).pop();
+        context.autoPop();
       } else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
         showInfo();
       }
@@ -308,7 +309,7 @@ class GalleryViewerPage extends HookConsumerWidget {
           .watch(assetProvider.notifier)
           .toggleArchive([asset], !asset.isArchived);
       if (isParent) {
-        AutoRouter.of(context).pop();
+        context.autoPop();
         return;
       }
       removeAssetFromStack();
@@ -331,7 +332,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 
     handleActivities() {
       if (sharedAlbumId != null) {
-        AutoRouter.of(context).push(
+        context.autoPush(
           ActivitiesRoute(
             albumId: sharedAlbumId!,
             assetId: asset().remoteId,
@@ -514,7 +515,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                               stackElements.elementAt(stackIndex.value),
                             );
                         Navigator.pop(ctx);
-                        AutoRouter.of(context).pop();
+                        context.autoPop();
                       },
                       title: const Text(
                         "viewer_stack_use_as_main_asset",
@@ -541,7 +542,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                           childrenToRemove: [currentAsset],
                         );
                         Navigator.pop(ctx);
-                        AutoRouter.of(context).pop();
+                        context.autoPop();
                       } else {
                         await ref.read(assetStackServiceProvider).updateStack(
                           currentAsset,
@@ -569,7 +570,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                             childrenToRemove: stack,
                           );
                       Navigator.pop(ctx);
-                      AutoRouter.of(context).pop();
+                      context.autoPop();
                     },
                     title: const Text(
                       "viewer_unstack",
@@ -829,8 +830,8 @@ class GalleryViewerPage extends HookConsumerWidget {
                       placeholder: Image(
                         image: provider,
                         fit: BoxFit.fitWidth,
-                        height: MediaQuery.of(context).size.height,
-                        width: MediaQuery.of(context).size.width,
+                        height: context.height,
+                        width: context.width,
                         alignment: Alignment.center,
                       ),
                       onVideoEnded: () {

+ 6 - 5
mobile/lib/modules/asset_viewer/views/video_viewer_page.dart

@@ -3,6 +3,7 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:chewie/chewie.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
@@ -44,7 +45,7 @@ class VideoViewerPage extends HookConsumerWidget {
         ),
         error: (error, stackTrace) => Icon(
           Icons.image_not_supported_outlined,
-          color: Theme.of(context).primaryColor,
+          color: context.primaryColor,
         ),
         loading: () => const Center(
           child: SizedBox(
@@ -74,8 +75,8 @@ class VideoViewerPage extends HookConsumerWidget {
         ),
         if (downloadAssetStatus == DownloadAssetStatus.loading)
           SizedBox(
-            height: MediaQuery.of(context).size.height,
-            width: MediaQuery.of(context).size.width,
+            height: context.height,
+            width: context.width,
             child: const Center(
               child: ImmichLoadingIndicator(),
             ),
@@ -205,8 +206,8 @@ class _VideoPlayerState extends State<VideoPlayer> {
       );
     } else {
       return SizedBox(
-        height: MediaQuery.of(context).size.height,
-        width: MediaQuery.of(context).size.width,
+        height: context.height,
+        width: context.width,
         child: Center(
           child: Stack(
             children: [

+ 7 - 7
mobile/lib/modules/backup/ui/album_info_card.dart

@@ -1,9 +1,9 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -22,10 +22,10 @@ class AlbumInfoCard extends HookConsumerWidget {
         ref.watch(backupProvider).selectedBackupAlbums.contains(albumInfo);
     final bool isExcluded =
         ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+    final isDarkTheme = context.isDarkTheme;
 
     ColorFilter selectedFilter = ColorFilter.mode(
-      Theme.of(context).primaryColor.withAlpha(100),
+      context.primaryColor.withAlpha(100),
       BlendMode.darken,
     );
     ColorFilter excludedFilter =
@@ -46,7 +46,7 @@ class AlbumInfoCard extends HookConsumerWidget {
               fontWeight: FontWeight.bold,
             ),
           ).tr(),
-          backgroundColor: Theme.of(context).primaryColor,
+          backgroundColor: context.primaryColor,
         );
       } else if (isExcluded) {
         return Chip(
@@ -194,7 +194,7 @@ class AlbumInfoCard extends HookConsumerWidget {
                           albumInfo.name,
                           style: TextStyle(
                             fontSize: 14,
-                            color: Theme.of(context).primaryColor,
+                            color: context.primaryColor,
                             fontWeight: FontWeight.bold,
                           ),
                         ),
@@ -224,13 +224,13 @@ class AlbumInfoCard extends HookConsumerWidget {
                   ),
                   IconButton(
                     onPressed: () {
-                      AutoRouter.of(context).push(
+                      context.autoPush(
                         AlbumPreviewRoute(album: albumInfo.albumEntity),
                       );
                     },
                     icon: Icon(
                       Icons.image_outlined,
-                      color: Theme.of(context).primaryColor,
+                      color: context.primaryColor,
                       size: 24,
                     ),
                     splashRadius: 25,

+ 8 - 9
mobile/lib/modules/backup/ui/album_info_list_tile.dart

@@ -1,10 +1,10 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -25,14 +25,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
         ref.watch(backupProvider).excludedBackupAlbums.contains(albumInfo);
 
     ColorFilter selectedFilter = ColorFilter.mode(
-      Theme.of(context).primaryColor.withAlpha(100),
+      context.primaryColor.withAlpha(100),
       BlendMode.darken,
     );
     ColorFilter excludedFilter =
         ColorFilter.mode(Colors.red.withAlpha(75), BlendMode.darken);
     ColorFilter unselectedFilter =
         const ColorFilter.mode(Colors.black, BlendMode.color);
-    var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 
     var assetCount = useState(0);
 
@@ -56,11 +55,11 @@ class AlbumInfoListTile extends HookConsumerWidget {
 
     buildTileColor() {
       if (isSelected) {
-        return isDarkTheme
-            ? Theme.of(context).primaryColor.withAlpha(100)
-            : Theme.of(context).primaryColor.withAlpha(25);
+        return context.isDarkTheme
+            ? context.primaryColor.withAlpha(100)
+            : context.primaryColor.withAlpha(25);
       } else if (isExcluded) {
-        return isDarkTheme
+        return context.isDarkTheme
             ? Colors.red[300]?.withAlpha(150)
             : Colors.red[100]?.withAlpha(150);
       } else {
@@ -159,13 +158,13 @@ class AlbumInfoListTile extends HookConsumerWidget {
         subtitle: Text(assetCount.value.toString()),
         trailing: IconButton(
           onPressed: () {
-            AutoRouter.of(context).push(
+            context.autoPush(
               AlbumPreviewRoute(album: albumInfo.albumEntity),
             );
           },
           icon: Icon(
             Icons.image_outlined,
-            color: Theme.of(context).primaryColor,
+            color: context.primaryColor,
             size: 24,
           ),
           splashRadius: 25,

+ 2 - 3
mobile/lib/modules/backup/ui/backup_info_card.dart

@@ -1,5 +1,6 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 class BackupInfoCard extends StatelessWidget {
   final String title;
@@ -14,13 +15,11 @@ class BackupInfoCard extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
-
     return Card(
       shape: RoundedRectangleBorder(
         borderRadius: BorderRadius.circular(20), // if you need this
         side: BorderSide(
-          color: isDarkMode
+          color: context.isDarkTheme
               ? const Color.fromARGB(255, 56, 56, 56)
               : Colors.black12,
           width: 1,

+ 5 - 5
mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart

@@ -1,9 +1,9 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
@@ -53,7 +53,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
         ),
         backgroundColor: Colors.white,
         onPressed: () {
-          AutoRouter.of(context).push(const FailedBackupStatusRoute());
+          context.autoPush(const FailedBackupStatusRoute());
         },
       );
     }
@@ -61,7 +61,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
     Widget buildAssetInfoTable() {
       return Table(
         border: TableBorder.all(
-          color: Theme.of(context).primaryColorLight,
+          color: context.themeData.primaryColorLight,
           width: 1,
         ),
         children: [
@@ -176,7 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
             onTap: () => isShowThumbnail.value = true,
             child: Icon(
               Icons.image_outlined,
-              color: Theme.of(context).primaryColor,
+              color: context.primaryColor,
               size: 30,
             ),
           ),
@@ -206,7 +206,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
                       minHeight: 10.0,
                       value: uploadProgress / 100.0,
                       backgroundColor: Colors.grey,
-                      color: Theme.of(context).primaryColor,
+                      color: context.primaryColor,
                     ),
                   ),
                   Text(

+ 3 - 2
mobile/lib/modules/backup/ui/ios_debug_info_tile.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
 import 'package:intl/intl.dart';
 
@@ -43,7 +44,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
         style: TextStyle(
           fontWeight: FontWeight.bold,
           fontSize: 14,
-          color: Theme.of(context).primaryColor,
+          color: context.primaryColor,
         ),
       ),
       subtitle: Text(
@@ -54,7 +55,7 @@ class IosDebugInfoTile extends HookConsumerWidget {
       ),
       leading: Icon(
         Icons.bug_report,
-        color: Theme.of(context).primaryColor,
+        color: context.primaryColor,
       ),
     );
   }

+ 2 - 2
mobile/lib/modules/backup/views/album_preview_page.dart

@@ -1,9 +1,9 @@
 import 'dart:typed_data';
 
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:photo_manager/photo_manager.dart';
 
@@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
           ],
         ),
         leading: IconButton(
-          onPressed: () => AutoRouter.of(context).pop(),
+          onPressed: () => context.autoPop(),
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
         ),
       ),

+ 9 - 9
mobile/lib/modules/backup/views/backup_album_selection_page.dart

@@ -1,9 +1,9 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
 import 'package:immich_mobile/modules/backup/ui/album_info_list_tile.dart';
@@ -17,7 +17,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
     // final availableAlbums = ref.watch(backupProvider).availableAlbums;
     final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
     final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
-    final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+    final isDarkTheme = context.isDarkTheme;
     final allAlbums = ref.watch(backupProvider).availableAlbums;
 
     // Albums which are displayed to the user
@@ -117,7 +117,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                   fontWeight: FontWeight.bold,
                 ),
               ),
-              backgroundColor: Theme.of(context).primaryColor,
+              backgroundColor: context.primaryColor,
               deleteIconColor: isDarkTheme ? Colors.black : Colors.white,
               deleteIcon: const Icon(
                 Icons.cancel_rounded,
@@ -147,12 +147,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                 album.name,
                 style: TextStyle(
                   fontSize: 10,
-                  color: Theme.of(context).colorScheme.surface,
+                  color: context.colorScheme.surface,
                   fontWeight: FontWeight.bold,
                 ),
               ),
               backgroundColor: Colors.red[300],
-              deleteIconColor: Theme.of(context).colorScheme.surface,
+              deleteIconColor: context.colorScheme.surface,
               deleteIcon: const Icon(
                 Icons.cancel_rounded,
                 size: 15,
@@ -209,7 +209,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
     return Scaffold(
       appBar: AppBar(
         leading: IconButton(
-          onPressed: () => AutoRouter.of(context).pop(),
+          onPressed: () => context.autoPop(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         title: const Text(
@@ -313,7 +313,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                       "backup_album_selection_page_albums_tap",
                       style: TextStyle(
                         fontSize: 12,
-                        color: Theme.of(context).primaryColor,
+                        color: context.primaryColor,
                         fontWeight: FontWeight.bold,
                       ),
                     ).tr(),
@@ -323,7 +323,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                     icon: Icon(
                       Icons.info,
                       size: 20,
-                      color: Theme.of(context).primaryColor,
+                      color: context.primaryColor,
                     ),
                     onPressed: () {
                       // show the dialog
@@ -340,7 +340,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
                               style: TextStyle(
                                 fontSize: 16,
                                 fontWeight: FontWeight.bold,
-                                color: Theme.of(context).primaryColor,
+                                color: context.primaryColor,
                               ),
                             ).tr(),
                             content: SingleChildScrollView(

+ 11 - 12
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -1,11 +1,11 @@
 import 'dart:io';
 
-import 'package:auto_route/auto_route.dart';
 import 'package:connectivity_plus/connectivity_plus.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
 import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
@@ -49,7 +49,6 @@ class BackupControllerPage extends HookConsumerWidget {
             !hasExclusiveAccess
         ? false
         : true;
-    var isDarkMode = Theme.of(context).brightness == Brightness.dark;
     final checkInProgress = useState(false);
 
     useEffect(
@@ -151,7 +150,7 @@ class BackupControllerPage extends HookConsumerWidget {
       return ListTile(
         leading: Icon(
           Icons.warning_rounded,
-          color: Theme.of(context).primaryColor,
+          color: context.primaryColor,
         ),
         title: const Text(
           "Check for corrupt asset backups",
@@ -187,7 +186,7 @@ class BackupControllerPage extends HookConsumerWidget {
         leading: isAutoBackup
             ? Icon(
                 Icons.cloud_done_rounded,
-                color: Theme.of(context).primaryColor,
+                color: context.primaryColor,
               )
             : const Icon(Icons.cloud_off_rounded),
         title: Text(
@@ -266,7 +265,7 @@ class BackupControllerPage extends HookConsumerWidget {
                   style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
                 ).tr(),
                 onPressed: () {
-                  Navigator.of(context).pop();
+                  context.pop();
                 },
               ),
             ],
@@ -279,7 +278,7 @@ class BackupControllerPage extends HookConsumerWidget {
       final bool isBackgroundEnabled = backupState.backgroundBackup;
       final bool isWifiRequired = backupState.backupRequireWifi;
       final bool isChargingRequired = backupState.backupRequireCharging;
-      final Color activeColor = Theme.of(context).primaryColor;
+      final Color activeColor = context.primaryColor;
 
       String formatBackupDelaySliderValue(double v) {
         if (v == 0.0) {
@@ -410,7 +409,7 @@ class BackupControllerPage extends HookConsumerWidget {
                       max: 3.0,
                       divisions: 3,
                       label: formatBackupDelaySliderValue(triggerDelay.value),
-                      activeColor: Theme.of(context).primaryColor,
+                      activeColor: context.primaryColor,
                     ),
                   ),
                 ElevatedButton(
@@ -511,7 +510,7 @@ class BackupControllerPage extends HookConsumerWidget {
           child: Text(
             text.trim().substring(0, text.length - 2),
             style: TextStyle(
-              color: Theme.of(context).primaryColor,
+              color: context.primaryColor,
               fontSize: 12,
               fontWeight: FontWeight.bold,
             ),
@@ -523,7 +522,7 @@ class BackupControllerPage extends HookConsumerWidget {
           child: Text(
             "backup_controller_page_none_selected".tr(),
             style: TextStyle(
-              color: Theme.of(context).primaryColor,
+              color: context.primaryColor,
               fontSize: 12,
               fontWeight: FontWeight.bold,
             ),
@@ -562,7 +561,7 @@ class BackupControllerPage extends HookConsumerWidget {
         shape: RoundedRectangleBorder(
           borderRadius: BorderRadius.circular(20),
           side: BorderSide(
-            color: isDarkMode
+            color: context.isDarkTheme
                 ? const Color.fromARGB(255, 56, 56, 56)
                 : Colors.black12,
             width: 1,
@@ -592,7 +591,7 @@ class BackupControllerPage extends HookConsumerWidget {
           ),
           trailing: ElevatedButton(
             onPressed: () {
-              AutoRouter.of(context).push(const BackupAlbumSelectionRoute());
+              context.autoPush(const BackupAlbumSelectionRoute());
             },
             child: const Text(
               "backup_controller_page_select",
@@ -678,7 +677,7 @@ class BackupControllerPage extends HookConsumerWidget {
         leading: IconButton(
           onPressed: () {
             ref.watch(websocketProvider.notifier).listenUploadEvent();
-            AutoRouter.of(context).pop(true);
+            context.autoPop(true);
           },
           splashRadius: 24,
           icon: const Icon(

+ 3 - 3
mobile/lib/modules/backup/views/failed_backup_status_page.dart

@@ -1,6 +1,6 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
 import 'package:intl/intl.dart';
 import 'package:photo_manager/photo_manager.dart';
@@ -20,7 +20,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
         ),
         leading: IconButton(
           onPressed: () {
-            AutoRouter.of(context).pop(true);
+            context.autoPop(true);
           },
           splashRadius: 24,
           icon: const Icon(
@@ -114,7 +114,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
                               style: TextStyle(
                                 fontWeight: FontWeight.bold,
                                 fontSize: 12,
-                                color: Theme.of(context).primaryColor,
+                                color: context.primaryColor,
                               ),
                             ),
                           ),

+ 2 - 2
mobile/lib/modules/favorite/views/favorites_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -28,7 +28,7 @@ class FavoritesPage extends HookConsumerWidget {
     AppBar buildAppBar() {
       return AppBar(
         leading: IconButton(
-          onPressed: () => AutoRouter.of(context).pop(),
+          onPressed: () => context.autoPop(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         centerTitle: true,

+ 3 - 2
mobile/lib/modules/home/ui/asset_grid/disable_multi_select_button.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 class DisableMultiSelectButton extends StatelessWidget {
   const DisableMultiSelectButton({
@@ -18,8 +19,8 @@ class DisableMultiSelectButton extends StatelessWidget {
         padding: const EdgeInsets.symmetric(horizontal: 4.0),
         child: ElevatedButton.icon(
           style: ElevatedButton.styleFrom(
-            foregroundColor: Theme.of(context).colorScheme.surface,
-            backgroundColor: Theme.of(context).colorScheme.primary,
+            foregroundColor: context.colorScheme.surface,
+            backgroundColor: context.primaryColor,
           ),
           onPressed: () {
             onPressed();

+ 3 - 5
mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 class GroupDividerTitle extends ConsumerWidget {
   const GroupDividerTitle({
@@ -51,14 +52,11 @@ class GroupDividerTitle extends ConsumerWidget {
             child: multiselectEnabled && selected
                 ? Icon(
                     Icons.check_circle_rounded,
-                    color: Theme.of(context).primaryColor,
+                    color: context.primaryColor,
                   )
                 : Icon(
                     Icons.check_circle_outline_rounded,
-                    color: Theme.of(context)
-                        .colorScheme
-                        .onBackground
-                        .withAlpha(100),
+                    color: context.colorScheme.onBackground.withAlpha(100),
                   ),
           ),
         ],

+ 7 - 6
mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart

@@ -4,10 +4,11 @@ import 'dart:math';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/utils/builtin_extensions.dart';
+import 'package:immich_mobile/extensions/collection_extensions.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 import 'asset_grid_data_structure.dart';
 import 'group_divider_title.dart';
@@ -221,7 +222,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
       padding: const EdgeInsets.only(left: 12.0, top: 24.0),
       child: Text(
         title,
-        style: Theme.of(context).textTheme.titleLarge,
+        style: context.textTheme.displayLarge,
       ),
     );
   }
@@ -239,7 +240,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
               bottom: widget.margin,
               right: i + 1 == num ? 0.0 : widget.margin,
             ),
-            color: Theme.of(context).colorScheme.surfaceVariant,
+            color: context.colorScheme.surfaceVariant,
           ),
       ],
     );
@@ -325,7 +326,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
     return Text(
       DateFormat.yMMMM().format(date),
       style: TextStyle(
-        color: Theme.of(context).colorScheme.onPrimary,
+        color: context.colorScheme.onPrimary,
         fontWeight: FontWeight.bold,
       ),
     );
@@ -368,8 +369,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
             scrollStateListener: dragScrolling,
             itemPositionsListener: _itemPositionsListener,
             controller: _itemScrollController,
-            backgroundColor: Theme.of(context).colorScheme.primary,
-            foregroundColor: Theme.of(context).colorScheme.onPrimary,
+            backgroundColor: context.primaryColor,
+            foregroundColor: context.colorScheme.onPrimary,
             labelTextBuilder: _labelBuilder,
             labelConstraints: const BoxConstraints(maxHeight: 28),
             scrollbarAnimationDuration: const Duration(milliseconds: 300),

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

@@ -1,6 +1,6 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -51,13 +51,13 @@ class ThumbnailImage extends StatelessWidget {
         return Container(
           decoration: BoxDecoration(
             shape: BoxShape.circle,
-            color: Theme.of(context).colorScheme.surfaceVariant,
+            color: context.colorScheme.surfaceVariant,
           ),
           child: Icon(
             Icons.check_circle_rounded,
             color: onDeselect == null
-                ? Theme.of(context).colorScheme.primary.withAlpha(120)
-                : Theme.of(context).colorScheme.primary,
+                ? context.primaryColor.withAlpha(120)
+                : context.primaryColor,
           ),
         );
       } else {
@@ -134,7 +134,7 @@ class ThumbnailImage extends StatelessWidget {
       final image = Container(
         width: 300,
         height: 300,
-        color: Theme.of(context).colorScheme.surfaceVariant,
+        color: context.colorScheme.surfaceVariant,
         child: Hero(
           tag: isFromDto
               ? '${asset.remoteId}-$heroOffset'
@@ -153,9 +153,9 @@ class ThumbnailImage extends StatelessWidget {
         decoration: BoxDecoration(
           border: Border.all(
             width: 0,
-            color: Theme.of(context).colorScheme.surfaceVariant,
+            color: context.colorScheme.surfaceVariant,
           ),
-          color: Theme.of(context).colorScheme.surfaceVariant,
+          color: context.colorScheme.surfaceVariant,
         ),
         child: ClipRRect(
           borderRadius: const BorderRadius.only(
@@ -178,7 +178,7 @@ class ThumbnailImage extends StatelessWidget {
             onSelect?.call();
           }
         } else {
-          AutoRouter.of(context).push(
+          context.autoPush(
             GalleryViewerRoute(
               initialIndex: index,
               loadAsset: loadAsset,
@@ -203,7 +203,7 @@ class ThumbnailImage extends StatelessWidget {
             decoration: BoxDecoration(
               border: multiselectEnabled && isSelected
                   ? Border.all(
-                      color: Theme.of(context).colorScheme.surfaceVariant,
+                      color: context.colorScheme.surfaceVariant,
                       width: 12,
                     )
                   : const Border(),

+ 3 - 2
mobile/lib/modules/home/ui/control_bottom_app_bar.dart

@@ -1,6 +1,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
 import 'package:immich_mobile/modules/home/models/selection_state.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
@@ -208,12 +209,12 @@ class AddToAlbumTitleRow extends StatelessWidget {
             onPressed: onCreateNewAlbum,
             icon: Icon(
               Icons.add,
-              color: Theme.of(context).primaryColor,
+              color: context.primaryColor,
             ),
             label: Text(
               "common_create_new_album",
               style: TextStyle(
-                color: Theme.of(context).primaryColor,
+                color: context.primaryColor,
                 fontWeight: FontWeight.bold,
                 fontSize: 14,
               ),

+ 4 - 5
mobile/lib/modules/home/views/home_page.dart

@@ -1,12 +1,12 @@
 import 'dart:async';
 
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -106,8 +106,7 @@ class HomePage extends HookConsumerWidget {
           handleShareAssets(ref, context, selection.value.toList());
         } else {
           final ids = remoteOnlySelection().map((e) => e.remoteId!);
-          AutoRouter.of(context)
-              .push(SharedLinkEditRoute(assetsList: ids.toList()));
+          context.autoPush(SharedLinkEditRoute(assetsList: ids.toList()));
         }
         processing.value = false;
         selectionEnabledHook.value = false;
@@ -243,7 +242,7 @@ class HomePage extends HookConsumerWidget {
             ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
             selectionEnabledHook.value = false;
 
-            AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
+            context.autoPush(AlbumViewerRoute(albumId: result.id));
           }
         } finally {
           processing.value = false;
@@ -300,7 +299,7 @@ class HomePage extends HookConsumerWidget {
                   style: TextStyle(
                     fontWeight: FontWeight.w600,
                     fontSize: 16,
-                    color: Theme.of(context).primaryColor,
+                    color: context.primaryColor,
                   ),
                 ).tr(),
               ),

+ 3 - 2
mobile/lib/modules/login/ui/change_password_form.dart

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
@@ -37,7 +38,7 @@ class ChangePasswordForm extends HookConsumerWidget {
                 style: TextStyle(
                   fontSize: 24,
                   fontWeight: FontWeight.bold,
-                  color: Theme.of(context).primaryColor,
+                  color: context.primaryColor,
                 ),
               ),
               Padding(
@@ -191,7 +192,7 @@ class ChangePasswordButton extends ConsumerWidget {
     return ElevatedButton(
       style: ElevatedButton.styleFrom(
         visualDensity: VisualDensity.standard,
-        backgroundColor: Theme.of(context).primaryColor,
+        backgroundColor: context.primaryColor,
         foregroundColor: Colors.grey[50],
         elevation: 2,
         padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),

+ 9 - 15
mobile/lib/modules/login/ui/login_form.dart

@@ -1,9 +1,9 @@
 import 'dart:io';
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/login/providers/oauth.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -150,7 +150,7 @@ class LoginForm extends HookConsumerWidget {
           // Resume backup (if enable) then navigate
           if (ref.read(authenticationProvider).shouldChangePassword &&
               !ref.read(authenticationProvider).isAdmin) {
-            AutoRouter.of(context).push(const ChangePasswordRoute());
+            context.autoPush(const ChangePasswordRoute());
           } else {
             final hasPermission = await ref
                 .read(galleryPermissionNotifier.notifier)
@@ -159,7 +159,7 @@ class LoginForm extends HookConsumerWidget {
               // Don't resume the backup until we have gallery permission
               ref.read(backupProvider.notifier).resumeBackup();
             }
-            AutoRouter.of(context).replace(const TabControllerRoute());
+            context.autoReplace(const TabControllerRoute());
           }
         } else {
           ImmichToast.show(
@@ -212,9 +212,7 @@ class LoginForm extends HookConsumerWidget {
             if (permission.isGranted || permission.isLimited) {
               ref.watch(backupProvider.notifier).resumeBackup();
             }
-            AutoRouter.of(context).replace(
-              const TabControllerRoute(),
-            );
+            context.autoReplace(const TabControllerRoute());
           } else {
             ImmichToast.show(
               context: context,
@@ -260,8 +258,7 @@ class LoginForm extends HookConsumerWidget {
                       ),
                     ),
                   ),
-                  onPressed: () =>
-                      AutoRouter.of(context).push(const SettingsRoute()),
+                  onPressed: () => context.autoPush(const SettingsRoute()),
                   icon: const Icon(Icons.settings_rounded),
                   label: const SizedBox.shrink(),
                 ),
@@ -303,10 +300,8 @@ class LoginForm extends HookConsumerWidget {
           children: [
             Text(
               serverEndpointController.text,
-              style: Theme.of(context)
-                  .textTheme
-                  .displaySmall
-                  ?.copyWith(color: Theme.of(context).primaryColor),
+              style: context.textTheme.displaySmall
+                  ?.copyWith(color: context.primaryColor),
               textAlign: TextAlign.center,
             ),
             if (isPasswordLoginEnable.value) ...[
@@ -342,8 +337,7 @@ class LoginForm extends HookConsumerWidget {
                               horizontal: 16.0,
                             ),
                             child: Divider(
-                              color: Brightness.dark ==
-                                      Theme.of(context).brightness
+                              color: context.isDarkTheme
                                   ? Colors.white
                                   : Colors.black,
                             ),
@@ -591,7 +585,7 @@ class OAuthLoginButton extends ConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     return ElevatedButton.icon(
       style: ElevatedButton.styleFrom(
-        backgroundColor: Theme.of(context).primaryColor.withAlpha(230),
+        backgroundColor: context.primaryColor.withAlpha(230),
         padding: const EdgeInsets.symmetric(vertical: 12),
       ),
       onPressed: onPressed,

+ 3 - 3
mobile/lib/modules/login/views/login_page.dart

@@ -1,7 +1,7 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/login/ui/login_form.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:package_info_plus/package_info_plus.dart';
@@ -47,13 +47,13 @@ class LoginPage extends HookConsumerWidget {
                 child: Text(
                   'Logs',
                   style: TextStyle(
-                    color: Theme.of(context).primaryColor,
+                    color: context.primaryColor,
                     fontWeight: FontWeight.bold,
                     fontFamily: "Inconsolata",
                   ),
                 ),
                 onTap: () {
-                  AutoRouter.of(context).push(const AppLogRoute());
+                  context.autoPush(const AppLogRoute());
                 },
               ),
             ],

+ 17 - 3
mobile/lib/modules/map/models/map_state.model.dart

@@ -1,14 +1,20 @@
+import 'package:vector_map_tiles/vector_map_tiles.dart';
+
 class MapState {
   final bool isDarkTheme;
   final bool showFavoriteOnly;
   final bool includeArchived;
   final int relativeTime;
+  final Style? mapStyle;
+  final bool isLoading;
 
   MapState({
     this.isDarkTheme = false,
     this.showFavoriteOnly = false,
     this.includeArchived = false,
     this.relativeTime = 0,
+    this.mapStyle,
+    this.isLoading = false,
   });
 
   MapState copyWith({
@@ -16,18 +22,22 @@ class MapState {
     bool? showFavoriteOnly,
     bool? includeArchived,
     int? relativeTime,
+    Style? mapStyle,
+    bool? isLoading,
   }) {
     return MapState(
       isDarkTheme: isDarkTheme ?? this.isDarkTheme,
       showFavoriteOnly: showFavoriteOnly ?? this.showFavoriteOnly,
       includeArchived: includeArchived ?? this.includeArchived,
       relativeTime: relativeTime ?? this.relativeTime,
+      mapStyle: mapStyle ?? this.mapStyle,
+      isLoading: isLoading ?? this.isLoading,
     );
   }
 
   @override
   String toString() {
-    return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived)';
+    return 'MapSettingsState(isDarkTheme: $isDarkTheme, showFavoriteOnly: $showFavoriteOnly, relativeTime: $relativeTime, includeArchived: $includeArchived, mapStyle: $mapStyle, isLoading: $isLoading)';
   }
 
   @override
@@ -38,7 +48,9 @@ class MapState {
         other.isDarkTheme == isDarkTheme &&
         other.showFavoriteOnly == showFavoriteOnly &&
         other.relativeTime == relativeTime &&
-        other.includeArchived == includeArchived;
+        other.includeArchived == includeArchived &&
+        other.mapStyle == mapStyle &&
+        other.isLoading == isLoading;
   }
 
   @override
@@ -46,6 +58,8 @@ class MapState {
     return isDarkTheme.hashCode ^
         showFavoriteOnly.hashCode ^
         relativeTime.hashCode ^
-        includeArchived.hashCode;
+        includeArchived.hashCode ^
+        mapStyle.hashCode ^
+        isLoading.hashCode;
   }
 }

+ 104 - 4
mobile/lib/modules/map/providers/map_state.provider.dart

@@ -1,10 +1,23 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_map/flutter_map.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/map/models/map_state.model.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/shared/providers/api.provider.dart';
+import 'package:immich_mobile/shared/services/api.service.dart';
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/utils/color_filter_generator.dart';
+import 'package:logging/logging.dart';
+import 'package:openapi/api.dart';
+import 'package:vector_map_tiles/vector_map_tiles.dart';
 
 class MapStateNotifier extends StateNotifier<MapState> {
-  MapStateNotifier(this._appSettingsProvider)
+  MapStateNotifier(this._appSettingsProvider, this._apiService)
       : super(
           MapState(
             isDarkTheme: _appSettingsProvider
@@ -15,17 +28,69 @@ class MapStateNotifier extends StateNotifier<MapState> {
                 .getSetting<bool>(AppSettingsEnum.mapIncludeArchived),
             relativeTime: _appSettingsProvider
                 .getSetting<int>(AppSettingsEnum.mapRelativeDate),
+            isLoading: true,
           ),
-        );
+        ) {
+    _fetchStyleFromServer(
+      _appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapThemeMode),
+    );
+  }
 
   final AppSettingsService _appSettingsProvider;
+  final ApiService _apiService;
+  final Logger _log = Logger("MapStateNotifier");
+
+  bool get isRaster =>
+      state.mapStyle != null && state.mapStyle!.rasterTileProvider != null;
+
+  double get maxZoom =>
+      (isRaster ? state.mapStyle!.rasterTileProvider!.maximumZoom : 14)
+          .toDouble();
 
   void switchTheme(bool isDarkTheme) {
+    _updateThemeMode(isDarkTheme);
+    _fetchStyleFromServer(isDarkTheme);
+  }
+
+  void _updateThemeMode(bool isDarkTheme) {
     _appSettingsProvider.setSetting(
       AppSettingsEnum.mapThemeMode,
       isDarkTheme,
     );
-    state = state.copyWith(isDarkTheme: isDarkTheme);
+    state = state.copyWith(isDarkTheme: isDarkTheme, isLoading: true);
+  }
+
+  void _fetchStyleFromServer(bool isDarkTheme) async {
+    final styleResponse = await _apiService.systemConfigApi
+        .getMapStyleWithHttpInfo(isDarkTheme ? MapTheme.dark : MapTheme.light);
+    if (styleResponse.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(styleResponse.statusCode, styleResponse.body);
+    }
+    final styleJsonString = styleResponse.body.isNotEmpty &&
+            styleResponse.statusCode != HttpStatus.noContent
+        ? styleResponse.body
+        : null;
+
+    if (styleJsonString == null) {
+      _log.severe('Style JSON from server is empty');
+      return;
+    }
+    final styleJson = await compute(jsonDecode, styleJsonString);
+    if (styleJson is! Map<String, dynamic>) {
+      _log.severe('Style JSON from server is invalid');
+      return;
+    }
+    final styleReader = StyleReader(uri: '');
+    Style? style;
+    try {
+      style = await styleReader.readFromMap(styleJson);
+    } finally {
+      // Consume all error
+    }
+    state = state.copyWith(
+      mapStyle: style,
+      isLoading: false,
+    );
   }
 
   void switchFavoriteOnly(bool isFavoriteOnly) {
@@ -51,9 +116,44 @@ class MapStateNotifier extends StateNotifier<MapState> {
     );
     state = state.copyWith(relativeTime: relativeTime);
   }
+
+  Widget getTileLayer([bool forceDark = false]) {
+    if (isRaster) {
+      final rasterProvider = state.mapStyle!.rasterTileProvider;
+      final rasterLayer = TileLayer(
+        urlTemplate: rasterProvider!.url,
+        maxNativeZoom: rasterProvider.maximumZoom,
+        maxZoom: rasterProvider.maximumZoom.toDouble(),
+      );
+      return state.isDarkTheme || forceDark
+          ? InvertionFilter(
+              child: SaturationFilter(
+                saturation: -1,
+                child: BrightnessFilter(
+                  brightness: -1,
+                  child: rasterLayer,
+                ),
+              ),
+            )
+          : rasterLayer;
+    }
+    if (state.mapStyle != null && !isRaster) {
+      return VectorTileLayer(
+        // Tiles and themes will be set for vector providers
+        tileProviders: state.mapStyle!.providers!,
+        theme: state.mapStyle!.theme!,
+        sprites: state.mapStyle!.sprites,
+        concurrency: 6,
+      );
+    }
+    return const Center(child: ImmichLoadingIndicator());
+  }
 }
 
 final mapStateNotifier =
     StateNotifierProvider<MapStateNotifier, MapState>((ref) {
-  return MapStateNotifier(ref.watch(appSettingsServiceProvider));
+  return MapStateNotifier(
+    ref.watch(appSettingsServiceProvider),
+    ref.watch(apiServiceProvider),
+  );
 });

+ 2 - 2
mobile/lib/modules/map/ui/map_page_app_bar.dart

@@ -1,8 +1,8 @@
 import 'dart:io';
 
-import 'package:auto_route/auto_route.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/modules/home/ui/asset_grid/disable_multi_select_button.dart';
 import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart';
 
@@ -30,7 +30,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget {
       Padding(
         padding: const EdgeInsets.only(left: 15, top: 15),
         child: ElevatedButton(
-          onPressed: () => AutoRouter.of(context).pop(),
+          onPressed: () => context.autoPop(),
           style: ElevatedButton.styleFrom(
             shape: const CircleBorder(),
             padding: const EdgeInsets.all(12),

+ 23 - 32
mobile/lib/modules/map/ui/map_page_bottom_sheet.dart

@@ -5,6 +5,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.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/immich_asset_grid.dart';
@@ -15,7 +16,6 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart';
 import 'package:immich_mobile/utils/color_filter_generator.dart';
 import 'package:immich_mobile/utils/debounce.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
-import 'package:url_launcher/url_launcher.dart';
 
 class MapPageBottomSheet extends StatefulHookConsumerWidget {
   final Stream mapPageEventStream;
@@ -57,10 +57,10 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
 
   @override
   Widget build(BuildContext context) {
-    final isDarkMode = Theme.of(context).brightness == Brightness.dark;
+    final isDarkTheme = context.isDarkTheme;
     final bottomPadding =
         Platform.isAndroid ? MediaQuery.of(context).padding.bottom - 10 : 0.0;
-    final maxHeight = MediaQuery.of(context).size.height - bottomPadding;
+    final maxHeight = context.height - bottomPadding;
     final isSheetScrolled = useState(false);
     final isSheetExpanded = useState(false);
     final assetsInBound = useState(<Asset>[]);
@@ -137,7 +137,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
                 SizedBox(
                   height: 150,
                   width: 150,
-                  child: isDarkMode
+                  child: isDarkTheme
                       ? const InvertionFilter(
                           child: SaturationFilter(
                             saturation: -1,
@@ -156,7 +156,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
                   "map_zoom_to_see_photos".tr(),
                   style: TextStyle(
                     fontSize: 20,
-                    color: Theme.of(context).textTheme.displayLarge?.color,
+                    color: context.textTheme.displayLarge?.color,
                   ),
                 ),
               ],
@@ -182,7 +182,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
         height: 60,
         width: double.infinity,
         decoration: BoxDecoration(
-          color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
+          color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
         ),
         child: Stack(
           children: [
@@ -197,17 +197,14 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
                   textToDisplay,
                   style: TextStyle(
                     fontSize: 16,
-                    color: Theme.of(context).textTheme.displayLarge?.color,
+                    color: context.textTheme.displayLarge?.color,
                     fontWeight: FontWeight.bold,
                   ),
                 ),
                 Divider(
                   height: 10,
-                  color: Theme.of(context)
-                      .textTheme
-                      .displayLarge
-                      ?.color
-                      ?.withOpacity(0.5),
+                  color:
+                      context.textTheme.displayLarge?.color?.withOpacity(0.5),
                 ),
               ],
             ),
@@ -218,7 +215,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
                 child: IconButton(
                   icon: Icon(
                     Icons.map_outlined,
-                    color: Theme.of(context).textTheme.displayLarge?.color,
+                    color: context.textTheme.displayLarge?.color,
                   ),
                   iconSize: 20,
                   tooltip: 'Zoom to bounds',
@@ -266,7 +263,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
                 ScrollController scrollController,
               ) {
                 return Card(
-                  color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
+                  color: isDarkTheme ? Colors.grey[900] : Colors.grey[100],
                   surfaceTintColor: Colors.transparent,
                   elevation: 18.0,
                   margin: const EdgeInsets.all(0),
@@ -320,24 +317,18 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
             Positioned(
               bottom: maxHeight * currentExtend.value,
               left: 0,
-              child: GestureDetector(
-                onTap: () => launchUrl(
-                  Uri.parse('https://openstreetmap.org/copyright'),
-                ),
-                child: ColoredBox(
-                  color: (widget.isDarkTheme
-                      ? Colors.grey[900]
-                      : Colors.grey[100])!,
-                  child: Padding(
-                    padding: const EdgeInsets.all(3),
-                    child: Text(
-                      '© OpenStreetMap contributors',
-                      style: TextStyle(
-                        fontSize: 6,
-                        color: !widget.isDarkTheme
-                            ? Colors.grey[900]
-                            : Colors.grey[100],
-                      ),
+              child: ColoredBox(
+                color:
+                    (widget.isDarkTheme ? Colors.grey[900] : Colors.grey[100])!,
+                child: Padding(
+                  padding: const EdgeInsets.all(3),
+                  child: Text(
+                    'OpenStreetMap contributors',
+                    style: TextStyle(
+                      fontSize: 6,
+                      color: !widget.isDarkTheme
+                          ? Colors.grey[900]
+                          : Colors.grey[100],
                     ),
                   ),
                 ),

+ 5 - 4
mobile/lib/modules/map/ui/map_settings_dialog.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
 
 class MapSettingsDialog extends HookConsumerWidget {
@@ -15,7 +16,7 @@ class MapSettingsDialog extends HookConsumerWidget {
     final showFavoriteOnly = useState(mapSettings.showFavoriteOnly);
     final showIncludeArchived = useState(mapSettings.includeArchived);
     final showRelativeDate = useState(mapSettings.relativeTime);
-    final ThemeData theme = Theme.of(context);
+    final ThemeData theme = context.themeData;
 
     Widget buildMapThemeSetting() {
       return SwitchListTile.adaptive(
@@ -125,7 +126,7 @@ class MapSettingsDialog extends HookConsumerWidget {
     List<Widget> getDialogActions() {
       return <Widget>[
         TextButton(
-          onPressed: () => Navigator.of(context).pop(),
+          onPressed: () => context.pop(),
           style: TextButton.styleFrom(
             backgroundColor:
                 mapSettings.isDarkTheme ? Colors.grey[100] : Colors.grey[700],
@@ -146,7 +147,7 @@ class MapSettingsDialog extends HookConsumerWidget {
             mapSettingsNotifier.setRelativeTime(showRelativeDate.value);
             mapSettingsNotifier
                 .switchIncludeArchived(showIncludeArchived.value);
-            Navigator.of(context).pop();
+            context.pop();
           },
           style: TextButton.styleFrom(
             backgroundColor: theme.primaryColor,
@@ -178,7 +179,7 @@ class MapSettingsDialog extends HookConsumerWidget {
         width: double.maxFinite,
         child: ConstrainedBox(
           constraints: BoxConstraints(
-            maxHeight: MediaQuery.of(context).size.height * 0.6,
+            maxHeight: context.height * 0.6,
           ),
           child: ListView(
             shrinkWrap: true,

+ 4 - 15
mobile/lib/modules/map/ui/map_thumbnail.dart

@@ -1,8 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_map/plugin_api.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/shared/providers/server_info.provider.dart';
-import 'package:immich_mobile/utils/color_filter_generator.dart';
+import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
 import 'package:latlong2/latlong.dart';
 import 'package:url_launcher/url_launcher.dart';
 
@@ -29,11 +28,7 @@ class MapThumbnail extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final tileLayer = TileLayer(
-      urlTemplate: ref.watch(
-        serverInfoProvider.select((v) => v.serverConfig.mapTileUrl),
-      ),
-    );
+    ref.watch(mapStateNotifier.select((s) => s.mapStyle));
 
     return SizedBox(
       height: height,
@@ -55,20 +50,14 @@ class MapThumbnail extends HookConsumerWidget {
                     'OpenStreetMap contributors',
                     onTap: () => launchUrl(
                       Uri.parse('https://openstreetmap.org/copyright'),
+                      mode: LaunchMode.externalApplication,
                     ),
                   ),
                 ],
               ),
           ],
           children: [
-            isDarkTheme
-                ? InvertionFilter(
-                    child: SaturationFilter(
-                      saturation: -1,
-                      child: tileLayer,
-                    ),
-                  )
-                : tileLayer,
+            ref.read(mapStateNotifier.notifier).getTileLayer(isDarkTheme),
             if (markers.isNotEmpty) MarkerLayer(markers: markers),
           ],
         ),

+ 62 - 72
mobile/lib/modules/map/views/map_page.dart

@@ -1,6 +1,6 @@
 import 'dart:async';
+import 'dart:math' as math;
 
-import 'package:auto_route/auto_route.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
@@ -11,6 +11,7 @@ import 'package:flutter_map_heatmap/flutter_map_heatmap.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:geolocator/geolocator.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/map/models/map_page_event.model.dart';
 import 'package:immich_mobile/modules/map/providers/map_marker.provider.dart';
 import 'package:immich_mobile/modules/map/providers/map_state.provider.dart';
@@ -20,12 +21,10 @@ import 'package:immich_mobile/modules/map/ui/map_page_bottom_sheet.dart';
 import 'package:immich_mobile/modules/map/ui/map_page_app_bar.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/asset.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/immich_toast.dart';
-import 'package:immich_mobile/utils/color_filter_generator.dart';
 import 'package:immich_mobile/utils/debounce.dart';
-import 'package:immich_mobile/utils/flutter_map_extensions.dart';
+import 'package:immich_mobile/extensions/flutter_map_extensions.dart';
 import 'package:immich_mobile/utils/immich_app_theme.dart';
 import 'package:immich_mobile/utils/selection_handlers.dart';
 import 'package:latlong2/latlong.dart';
@@ -79,26 +78,30 @@ class MapPageState extends ConsumerState<MapPage> {
     Set<AssetMarkerData>? assetMarkers, {
     bool forceReload = false,
   }) {
-    final bounds = mapController.bounds;
-    if (bounds != null) {
-      final oldAssetsInBounds = assetsInBounds.toSet();
-      assetsInBounds =
-          assetMarkers?.where((e) => bounds.contains(e.point)).toSet() ?? {};
-      final shouldReload = forceReload ||
-          assetsInBounds.difference(oldAssetsInBounds).isNotEmpty ||
-          assetsInBounds.length != oldAssetsInBounds.length;
-      if (shouldReload) {
-        mapPageEventSC.add(
-          MapPageAssetsInBoundUpdated(
-            assetsInBounds.map((e) => e.asset).toList(),
-          ),
-        );
+    try {
+      final bounds = mapController.bounds;
+      if (bounds != null) {
+        final oldAssetsInBounds = assetsInBounds.toSet();
+        assetsInBounds =
+            assetMarkers?.where((e) => bounds.contains(e.point)).toSet() ?? {};
+        final shouldReload = forceReload ||
+            assetsInBounds.difference(oldAssetsInBounds).isNotEmpty ||
+            assetsInBounds.length != oldAssetsInBounds.length;
+        if (shouldReload) {
+          mapPageEventSC.add(
+            MapPageAssetsInBoundUpdated(
+              assetsInBounds.map((e) => e.asset).toList(),
+            ),
+          );
+        }
       }
+    } finally {
+      // Consume all error
     }
   }
 
   void openAssetInViewer(Asset asset) {
-    AutoRouter.of(context).push(
+    context.autoPush(
       GalleryViewerRoute(
         initialIndex: 0,
         loadAsset: (index) => asset,
@@ -120,6 +123,10 @@ class MapPageState extends ConsumerState<MapPage> {
     final selectedAssets = useState(<Asset>{});
     final showLoadingIndicator = useState(false);
     final refetchMarkers = useState(true);
+    final isLoading =
+        ref.watch(mapStateNotifier.select((state) => state.isLoading));
+    final maxZoom = ref.read(mapStateNotifier.notifier).maxZoom;
+    final zoomLevel = math.min(maxZoom, 14.0);
     final themeData = isDarkTheme ? immichDarkTheme : immichLightTheme;
 
     if (refetchMarkers.value) {
@@ -169,7 +176,6 @@ class MapPageState extends ConsumerState<MapPage> {
         final mapMarker = mapMarkerData.value
             .firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
         if (mapMarker != null) {
-          const zoomLevel = 16.0;
           LatLng? newCenter = mapController.centerBoundsWithPadding(
             mapMarker.point,
             const Offset(0, -120),
@@ -231,7 +237,7 @@ class MapPageState extends ConsumerState<MapPage> {
         forceAssetUpdate = true;
         mapController.move(
           LatLng(currentUserLocation.latitude, currentUserLocation.longitude),
-          12,
+          zoomLevel,
         );
       } catch (error) {
         log.severe(
@@ -360,24 +366,6 @@ class MapPageState extends ConsumerState<MapPage> {
       selectedAssets.value = selection;
     }
 
-    final tileLayer = TileLayer(
-      urlTemplate: ref.watch(
-        serverInfoProvider.select((v) => v.serverConfig.mapTileUrl),
-      ),
-      maxNativeZoom: 19,
-      maxZoom: 19,
-    );
-
-    final darkTileLayer = InvertionFilter(
-      child: SaturationFilter(
-        saturation: -1,
-        child: BrightnessFilter(
-          brightness: -1,
-          child: tileLayer,
-        ),
-      ),
-    );
-
     final markerLayer = MarkerLayer(
       markers: [
         if (closestAssetMarker.value != null)
@@ -452,41 +440,43 @@ class MapPageState extends ConsumerState<MapPage> {
           extendBodyBehindAppBar: true,
           body: Stack(
             children: [
-              FlutterMap(
-                mapController: mapController,
-                options: MapOptions(
-                  maxBounds:
-                      LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
-                  interactiveFlags: InteractiveFlag.doubleTapZoom |
-                      InteractiveFlag.drag |
-                      InteractiveFlag.flingAnimation |
-                      InteractiveFlag.pinchMove |
-                      InteractiveFlag.pinchZoom,
-                  center: LatLng(20, 20),
-                  zoom: 2,
-                  minZoom: 1,
-                  maxZoom: 18, // max level supported by OSM,
-                  onMapReady: () {
-                    mapController.mapEventStream.listen(onMapEvent);
-                  },
+              if (!isLoading)
+                FlutterMap(
+                  mapController: mapController,
+                  options: MapOptions(
+                    maxBounds:
+                        LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)),
+                    interactiveFlags: InteractiveFlag.doubleTapZoom |
+                        InteractiveFlag.drag |
+                        InteractiveFlag.flingAnimation |
+                        InteractiveFlag.pinchMove |
+                        InteractiveFlag.pinchZoom,
+                    center: LatLng(20, 20),
+                    zoom: 2,
+                    minZoom: 1,
+                    maxZoom: maxZoom,
+                    onMapReady: () {
+                      mapController.mapEventStream.listen(onMapEvent);
+                    },
+                  ),
+                  children: [
+                    ref.read(mapStateNotifier.notifier).getTileLayer(),
+                    heatMapLayer,
+                    markerLayer,
+                  ],
                 ),
-                children: [
-                  isDarkTheme ? darkTileLayer : tileLayer,
-                  heatMapLayer,
-                  markerLayer,
-                ],
-              ),
-              MapPageBottomSheet(
-                mapPageEventStream: mapPageEventSC.stream,
-                bottomSheetEventSC: bottomSheetEventSC,
-                selectionEnabled: selectionEnabledHook.value,
-                selectionlistener: selectionListener,
-                isDarkTheme: isDarkTheme,
-              ),
-              if (showLoadingIndicator.value)
+              if (!isLoading)
+                MapPageBottomSheet(
+                  mapPageEventStream: mapPageEventSC.stream,
+                  bottomSheetEventSC: bottomSheetEventSC,
+                  selectionEnabled: selectionEnabledHook.value,
+                  selectionlistener: selectionListener,
+                  isDarkTheme: isDarkTheme,
+                ),
+              if (showLoadingIndicator.value || isLoading)
                 Positioned(
-                  top: MediaQuery.of(context).size.height * 0.35,
-                  left: MediaQuery.of(context).size.width * 0.425,
+                  top: context.height * 0.35,
+                  left: context.width * 0.425,
                   child: const ImmichLoadingIndicator(),
                 ),
             ],

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

@@ -1,7 +1,7 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget {
                         child: GestureDetector(
                           onTap: () {
                             HapticFeedback.heavyImpact();
-                            AutoRouter.of(context).push(
+                            context.autoPush(
                               MemoryRoute(
                                 memories: memories,
                                 memoryIndex: index,

+ 4 - 4
mobile/lib/modules/memories/views/memory_page.dart

@@ -1,8 +1,8 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/memories/models/memory.dart';
 import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -182,14 +182,14 @@ class MemoryPage extends HookConsumerWidget {
                   currentMemory.value.assets.length;
               if (isLastAsset &&
                   (offset > notification.metrics.maxScrollExtent + 150)) {
-                AutoRouter.of(context).pop();
+                context.autoPop();
                 return true;
               }
             }
             // Horizontal scroll handling
             if (notification.depth == 1 &&
                 (offset > notification.metrics.maxScrollExtent + 100)) {
-              AutoRouter.of(context).pop();
+              context.autoPop();
               return true;
             }
           }
@@ -244,7 +244,7 @@ class MemoryPage extends HookConsumerWidget {
                           child: MemoryCard(
                             asset: asset,
                             onTap: () => toNextAsset(index),
-                            onClose: () => AutoRouter.of(context).pop(),
+                            onClose: () => context.autoPop(),
                             rightCornerText: assetProgress.value,
                             title: memories[mIndex].title,
                             showTitle: index == 0,

+ 21 - 26
mobile/lib/modules/onboarding/views/permission_onboarding_page.dart

@@ -1,7 +1,7 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
@@ -11,7 +11,6 @@ import 'package:immich_mobile/shared/ui/immich_title_text.dart';
 import 'package:permission_handler/permission_handler.dart';
 
 class PermissionOnboardingPage extends HookConsumerWidget {
-
   const PermissionOnboardingPage({super.key});
 
   @override
@@ -21,13 +20,10 @@ class PermissionOnboardingPage extends HookConsumerWidget {
     // Navigate to the main Tab Controller when permission is granted
     void goToHome() {
       // Resume backup (if enable) then navigate
-      ref.watch(backupProvider.notifier).resumeBackup()
-        .catchError((error) {
+      ref.watch(backupProvider.notifier).resumeBackup().catchError((error) {
         debugPrint('PermissionOnboardingPage error: $error');
       });
-      AutoRouter.of(context).replace(
-        const TabControllerRoute(),
-      );
+      context.autoReplace(const TabControllerRoute());
     }
 
     // When the permission is denied, we show a request permission page
@@ -38,21 +34,21 @@ class PermissionOnboardingPage extends HookConsumerWidget {
         children: [
           Text(
             'permission_onboarding_request',
-            style: Theme.of(context).textTheme.titleMedium,
+            style: context.textTheme.titleMedium,
             textAlign: TextAlign.center,
           ).tr(),
           const SizedBox(height: 18),
           ElevatedButton(
             onPressed: () => ref
-              .read(galleryPermissionNotifier.notifier)
-              .requestGalleryPermission()
-              .then((permission) async {
-                if (permission.isGranted) {
-                  // If permission is limited, we will show the limited
-                  // permission page
-                  goToHome();
-                }
-              }),
+                .read(galleryPermissionNotifier.notifier)
+                .requestGalleryPermission()
+                .then((permission) async {
+              if (permission.isGranted) {
+                // If permission is limited, we will show the limited
+                // permission page
+                goToHome();
+              }
+            }),
             child: const Text(
               'permission_onboarding_grant_permission',
             ).tr(),
@@ -70,7 +66,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
         children: [
           Text(
             'permission_onboarding_permission_granted',
-            style: Theme.of(context).textTheme.titleMedium,
+            style: context.textTheme.titleMedium,
             textAlign: TextAlign.center,
           ).tr(),
           const SizedBox(height: 18),
@@ -90,14 +86,15 @@ class PermissionOnboardingPage extends HookConsumerWidget {
         crossAxisAlignment: CrossAxisAlignment.center,
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
-          const Icon(Icons.warning_outlined,
+          const Icon(
+            Icons.warning_outlined,
             color: Colors.yellow,
             size: 48,
           ),
           const SizedBox(height: 8),
           Text(
             'permission_onboarding_permission_limited',
-            style: Theme.of(context).textTheme.titleMedium,
+            style: context.textTheme.titleMedium,
             textAlign: TextAlign.center,
           ).tr(),
           const SizedBox(height: 18),
@@ -123,14 +120,15 @@ class PermissionOnboardingPage extends HookConsumerWidget {
         crossAxisAlignment: CrossAxisAlignment.center,
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
-          const Icon(Icons.warning_outlined,
+          const Icon(
+            Icons.warning_outlined,
             color: Colors.red,
             size: 48,
           ),
           const SizedBox(height: 8),
           Text(
             'permission_onboarding_permission_denied',
-            style: Theme.of(context).textTheme.titleMedium,
+            style: context.textTheme.titleMedium,
             textAlign: TextAlign.center,
           ).tr(),
           const SizedBox(height: 18),
@@ -186,13 +184,10 @@ class PermissionOnboardingPage extends HookConsumerWidget {
                   child: const Text('permission_onboarding_log_out').tr(),
                   onPressed: () {
                     ref.read(authenticationProvider.notifier).logout();
-                    AutoRouter.of(context).replace(
-                      const LoginRoute(),
-                    );
+                    context.autoReplace(const LoginRoute());
                   },
                 ),
               ],
-
             ),
           ),
         ),

+ 3 - 3
mobile/lib/modules/partner/ui/partner_list.dart

@@ -1,6 +1,6 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/ui/user_avatar.dart';
@@ -28,10 +28,10 @@ class PartnerList extends HookConsumerWidget {
         style: TextStyle(
           fontWeight: FontWeight.bold,
           fontSize: 14,
-          color: Theme.of(context).primaryColor,
+          color: context.primaryColor,
         ),
       ),
-      onTap: () => AutoRouter.of(context).push(PartnerDetailRoute(partner: p)),
+      onTap: () => context.autoPush((PartnerDetailRoute(partner: p))),
     );
   }
 }

+ 3 - 1
mobile/lib/modules/search/ui/curated_people_row.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.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/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/shared/models/store.dart';
@@ -68,6 +69,7 @@ class CuratedPeopleRow extends StatelessWidget {
                       elevation: 3,
                       child: CircleAvatar(
                         maxRadius: imageSize / 2,
+                        backgroundColor: context.colorScheme.surfaceVariant,
                         backgroundImage: NetworkImage(
                           getFaceThumbnailUrl(person.id),
                           headers: headers,
@@ -85,7 +87,7 @@ class CuratedPeopleRow extends StatelessWidget {
                         "Add name",
                         style: TextStyle(
                           fontWeight: FontWeight.bold,
-                          color: Theme.of(context).primaryColor,
+                          color: context.primaryColor,
                         ),
                       ),
                     ),

+ 18 - 15
mobile/lib/modules/search/ui/curated_places_row.dart

@@ -1,5 +1,5 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart';
 import 'package:immich_mobile/modules/search/ui/curated_row.dart';
 import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
@@ -25,7 +25,7 @@ class CuratedPlacesRow extends CuratedRow {
     final int actualContentIndex = isMapEnabled ? 1 : 0;
     Widget buildMapThumbnail() {
       return GestureDetector(
-        onTap: () => AutoRouter.of(context).push(
+        onTap: () => context.autoPush(
           const MapRoute(),
         ),
         child: SizedBox(
@@ -43,21 +43,24 @@ class CuratedPlacesRow extends CuratedRow {
                   ),
                   height: imageSize,
                   showAttribution: false,
-                  isDarkTheme: Theme.of(context).brightness == Brightness.dark,
+                  isDarkTheme: context.isDarkTheme,
                 ),
               ),
-              Container(
-                decoration: BoxDecoration(
-                  borderRadius: BorderRadius.circular(10),
-                  color: Colors.black,
-                  gradient: LinearGradient(
-                    begin: FractionalOffset.topCenter,
-                    end: FractionalOffset.bottomCenter,
-                    colors: [
-                      Colors.blueGrey.withOpacity(0.0),
-                      Colors.black.withOpacity(0.4),
-                    ],
-                    stops: const [0.0, 1.0],
+              Padding(
+                padding: const EdgeInsets.only(right: 10.0),
+                child: Container(
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(10),
+                    color: Colors.black,
+                    gradient: LinearGradient(
+                      begin: FractionalOffset.topCenter,
+                      end: FractionalOffset.bottomCenter,
+                      colors: [
+                        Colors.blueGrey.withOpacity(0.0),
+                        Colors.black.withOpacity(0.4),
+                      ],
+                      stops: const [0.0, 0.4],
+                    ),
                   ),
                 ),
               ),

+ 3 - 3
mobile/lib/modules/search/ui/explore_grid.dart

@@ -1,5 +1,5 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.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/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -50,13 +50,13 @@ class ExploreGrid extends StatelessWidget {
           borderRadius: 0,
           onTap: () {
             isPeople
-                ? AutoRouter.of(context).push(
+                ? context.autoPush(
                     PersonResultRoute(
                       personId: content.id,
                       personName: content.label,
                     ),
                   )
-                : AutoRouter.of(context).push(
+                : context.autoPush(
                     SearchResultRoute(searchTerm: 'm:${content.label}'),
                   );
           },

+ 6 - 5
mobile/lib/modules/search/ui/immich_search_bar.dart

@@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 
 class ImmichSearchBar extends HookConsumerWidget
@@ -57,11 +58,11 @@ class ImmichSearchBar extends HookConsumerWidget
         },
         decoration: InputDecoration(
           hintText: 'search_bar_hint'.tr(),
-          hintStyle: Theme.of(context).textTheme.titleSmall?.copyWith(
-                color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
-                fontWeight: FontWeight.w500,
-                fontSize: 14,
-              ),
+          hintStyle: context.textTheme.titleSmall?.copyWith(
+            color: context.themeData.colorScheme.onSurface.withOpacity(0.5),
+            fontWeight: FontWeight.w500,
+            fontSize: 14,
+          ),
           enabledBorder: const UnderlineInputBorder(
             borderSide: BorderSide(color: Colors.transparent),
           ),

+ 2 - 1
mobile/lib/modules/search/ui/person_name_edit_form.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 
 class PersonNameEditFormResult {
@@ -71,7 +72,7 @@ class PersonNameEditForm extends HookConsumerWidget {
           child: Text(
             "Save",
             style: TextStyle(
-              color: Theme.of(context).primaryColor,
+              color: context.primaryColor,
               fontWeight: FontWeight.bold,
             ),
           ),

+ 3 - 2
mobile/lib/modules/search/ui/search_row_title.dart

@@ -1,5 +1,6 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 
 class SearchRowTitle extends StatelessWidget {
   final Function() onViewAllPressed;
@@ -26,14 +27,14 @@ class SearchRowTitle extends StatelessWidget {
         children: [
           Text(
             title,
-            style: Theme.of(context).textTheme.titleSmall,
+            style: context.textTheme.titleSmall,
           ),
           TextButton(
             onPressed: onViewAllPressed,
             child: Text(
               'search_page_view_all_button',
               style: TextStyle(
-                color: Theme.of(context).primaryColor,
+                color: context.primaryColor,
                 fontWeight: FontWeight.bold,
                 fontSize: 14.0,
               ),

+ 8 - 8
mobile/lib/modules/search/ui/search_suggestion_list.dart

@@ -1,6 +1,7 @@
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 
 class SearchSuggestionList extends ConsumerWidget {
@@ -13,17 +14,16 @@ class SearchSuggestionList extends ConsumerWidget {
     final searchTerm = ref.watch(searchPageStateProvider).searchTerm;
     final searchSuggestion =
         ref.watch(searchPageStateProvider).searchSuggestion;
-    var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 
     return Container(
       color: searchTerm.isEmpty
           ? Colors.black.withOpacity(0.5)
-          : Theme.of(context).scaffoldBackgroundColor,
+          : context.scaffoldBackgroundColor,
       child: CustomScrollView(
         slivers: [
           SliverToBoxAdapter(
             child: Container(
-              color: isDarkTheme ? Colors.grey[800] : Colors.grey[100],
+              color: context.isDarkTheme ? Colors.grey[800] : Colors.grey[100],
               child: Padding(
                 padding: const EdgeInsets.all(16.0),
                 child: RichText(
@@ -31,14 +31,14 @@ class SearchSuggestionList extends ConsumerWidget {
                     children: [
                       TextSpan(
                         text: 'search_suggestion_list_smart_search_hint_1'.tr(),
-                        style: Theme.of(context).textTheme.bodyMedium,
+                        style: context.textTheme.bodyMedium,
                       ),
                       TextSpan(
                         text: 'search_suggestion_list_smart_search_hint_2'.tr(),
-                        style: Theme.of(context).textTheme.bodyMedium?.copyWith(
-                              color: Theme.of(context).primaryColor,
-                              fontWeight: FontWeight.bold,
-                            ),
+                        style: context.textTheme.bodyMedium?.copyWith(
+                          color: context.primaryColor,
+                          fontWeight: FontWeight.bold,
+                        ),
                       ),
                     ],
                   ),

+ 8 - 8
mobile/lib/modules/search/ui/thumbnail_with_info.dart

@@ -1,7 +1,8 @@
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/models/store.dart';
-import 'package:immich_mobile/utils/capitalize.dart';
+import 'package:immich_mobile/extensions/string_extensions.dart';
 
 // ignore: must_be_immutable
 class ThumbnailWithInfo extends StatelessWidget {
@@ -32,7 +33,7 @@ class ThumbnailWithInfo extends StatelessWidget {
           Container(
             decoration: BoxDecoration(
               borderRadius: BorderRadius.circular(borderRadius),
-              color: Theme.of(context).colorScheme.surfaceVariant,
+              color: context.colorScheme.surfaceVariant,
             ),
             child: imageUrl != null
                 ? ClipRRect(
@@ -46,8 +47,7 @@ class ThumbnailWithInfo extends StatelessWidget {
                           dimension: 250,
                           child: DecoratedBox(
                             decoration: BoxDecoration(
-                              color:
-                                  Theme.of(context).colorScheme.surfaceVariant,
+                              color: context.colorScheme.surfaceVariant,
                             ),
                           ),
                         );
@@ -64,7 +64,7 @@ class ThumbnailWithInfo extends StatelessWidget {
                 : Center(
                     child: Icon(
                       noImageIcon ?? Icons.not_listed_location,
-                      color: Theme.of(context).primaryColor,
+                      color: context.primaryColor,
                     ),
                   ),
           ),
@@ -76,10 +76,10 @@ class ThumbnailWithInfo extends StatelessWidget {
                 begin: FractionalOffset.topCenter,
                 end: FractionalOffset.bottomCenter,
                 colors: [
-                  Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.0),
+                  context.colorScheme.surfaceVariant.withOpacity(0.0),
                   textInfo == ''
-                      ? Theme.of(context).colorScheme.surface.withOpacity(0.1)
-                      : Theme.of(context).colorScheme.surface.withOpacity(0.6),
+                      ? context.colorScheme.surface.withOpacity(0.1)
+                      : context.colorScheme.surface.withOpacity(0.6),
                 ],
                 stops: const [0.0, 1.0],
               ),

+ 2 - 2
mobile/lib/modules/search/views/all_motion_videos_page.dart

@@ -1,7 +1,7 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/search/providers/all_motion_photos.provider.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
@@ -17,7 +17,7 @@ class AllMotionPhotosPage extends HookConsumerWidget {
       appBar: AppBar(
         title: const Text('motion_photos_page_title').tr(),
         leading: IconButton(
-          onPressed: () => AutoRouter.of(context).pop(),
+          onPressed: () => context.autoPop(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),

Some files were not shown because too many files changed in this diff