mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Optimize android's Gradle settings and clean up mobile source code (#240)
* optimize android side gradle settings * android minsdk back to 21 * remove unused package, update linter and fix lint error
This commit is contained in:
		@@ -1,10 +1,45 @@
 | 
			
		||||
# This file tracks properties of this Flutter project.
 | 
			
		||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
 | 
			
		||||
#
 | 
			
		||||
# This file should be version controlled and should not be manually edited.
 | 
			
		||||
# This file should be version controlled.
 | 
			
		||||
 | 
			
		||||
version:
 | 
			
		||||
  revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
 | 
			
		||||
  revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
  channel: stable
 | 
			
		||||
 | 
			
		||||
project_type: app
 | 
			
		||||
 | 
			
		||||
# Tracks metadata for the flutter migrate command
 | 
			
		||||
migration:
 | 
			
		||||
  platforms:
 | 
			
		||||
    - platform: root
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
    - platform: android
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
    - platform: ios
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
    - platform: linux
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
    - platform: macos
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
    - platform: web
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
    - platform: windows
 | 
			
		||||
      create_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
      base_revision: cd41fdd495f6944ecd3506c21e94c6567b073278
 | 
			
		||||
 | 
			
		||||
  # User provided section
 | 
			
		||||
 | 
			
		||||
  # List of Local paths (relative to this file) that should be
 | 
			
		||||
  # ignored by the migrate tool.
 | 
			
		||||
  #
 | 
			
		||||
  # Files that are not part of the templates will be ignored by default.
 | 
			
		||||
  unmanaged_files:
 | 
			
		||||
    - 'lib/main.dart'
 | 
			
		||||
    - 'ios/Runner.xcodeproj/project.pbxproj'
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ linter:
 | 
			
		||||
  rules:
 | 
			
		||||
    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
 | 
			
		||||
    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
 | 
			
		||||
    use_build_context_synchronously: false
 | 
			
		||||
 | 
			
		||||
# Additional information about this file can be found at
 | 
			
		||||
# https://dart.dev/guides/language/analysis-options
 | 
			
		||||
 
 | 
			
		||||
@@ -81,5 +81,4 @@ flutter {
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
 | 
			
		||||
    implementation 'com.android.support:multidex:1.0.3'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
// Generated file.
 | 
			
		||||
//
 | 
			
		||||
// If you wish to remove Flutter's multidex support, delete this entire file.
 | 
			
		||||
//
 | 
			
		||||
// Modifications to this file should be done in a copy under a different name
 | 
			
		||||
// as this file may be regenerated.
 | 
			
		||||
 | 
			
		||||
package io.flutter.app;
 | 
			
		||||
 | 
			
		||||
import android.app.Application;
 | 
			
		||||
import android.content.Context;
 | 
			
		||||
import androidx.annotation.CallSuper;
 | 
			
		||||
import androidx.multidex.MultiDex;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Extension of {@link android.app.Application}, adding multidex support.
 | 
			
		||||
 */
 | 
			
		||||
public class FlutterMultiDexApplication extends Application {
 | 
			
		||||
  @Override
 | 
			
		||||
  @CallSuper
 | 
			
		||||
  protected void attachBaseContext(Context base) {
 | 
			
		||||
    super.attachBaseContext(base);
 | 
			
		||||
    MultiDex.install(this);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ buildscript {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    dependencies {
 | 
			
		||||
        classpath 'com.android.tools.build:gradle:4.1.0'
 | 
			
		||||
        classpath 'com.android.tools.build:gradle:7.1.2'
 | 
			
		||||
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,5 @@ distributionBase=GRADLE_USER_HOME
 | 
			
		||||
distributionPath=wrapper/dists
 | 
			
		||||
zipStoreBase=GRADLE_USER_HOME
 | 
			
		||||
zipStorePath=wrapper/dists
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
 | 
			
		||||
distributionSha256Sum=0080de8491f0918e4f529a6db6820fa0b9e818ee2386117f4394f95feb1d5583
 | 
			
		||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
 | 
			
		||||
distributionSha256Sum=cd5c2958a107ee7f0722004a12d0f8559b4564c34daad7df06cffd4d12a426d0
 | 
			
		||||
@@ -42,10 +42,11 @@ class ImmichApp extends ConsumerStatefulWidget {
 | 
			
		||||
  const ImmichApp({Key? key}) : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  _ImmichAppState createState() => _ImmichAppState();
 | 
			
		||||
  ImmichAppState createState() => ImmichAppState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserver {
 | 
			
		||||
class ImmichAppState extends ConsumerState<ImmichApp>
 | 
			
		||||
    with WidgetsBindingObserver {
 | 
			
		||||
  @override
 | 
			
		||||
  void didChangeAppLifecycleState(AppLifecycleState state) {
 | 
			
		||||
    switch (state) {
 | 
			
		||||
@@ -121,7 +122,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
 | 
			
		||||
              brightness: Brightness.light,
 | 
			
		||||
              primarySwatch: Colors.indigo,
 | 
			
		||||
              fontFamily: 'WorkSans',
 | 
			
		||||
              snackBarTheme: const SnackBarThemeData(contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
 | 
			
		||||
              snackBarTheme: const SnackBarThemeData(
 | 
			
		||||
                  contentTextStyle: TextStyle(fontFamily: 'WorkSans')),
 | 
			
		||||
              scaffoldBackgroundColor: immichBackgroundColor,
 | 
			
		||||
              appBarTheme: const AppBarTheme(
 | 
			
		||||
                backgroundColor: immichBackgroundColor,
 | 
			
		||||
@@ -132,7 +134,8 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
            routeInformationParser: _immichRouter.defaultRouteParser(),
 | 
			
		||||
            routerDelegate: _immichRouter.delegate(navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
 | 
			
		||||
            routerDelegate: _immichRouter.delegate(
 | 
			
		||||
                navigatorObservers: () => [TabNavigationObserver(ref: ref)]),
 | 
			
		||||
          ),
 | 
			
		||||
          const ImmichLoadingOverlay(),
 | 
			
		||||
          const VersionAnnouncementOverlay(),
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,14 @@ import 'package:latlong2/latlong.dart';
 | 
			
		||||
class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
  final ImmichAssetWithExif assetDetail;
 | 
			
		||||
 | 
			
		||||
  const ExifBottomSheet({Key? key, required this.assetDetail}) : super(key: key);
 | 
			
		||||
  const ExifBottomSheet({Key? key, required this.assetDetail})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    _buildMap() {
 | 
			
		||||
      return (assetDetail.exifInfo!.latitude != null && assetDetail.exifInfo!.longitude != null)
 | 
			
		||||
      return (assetDetail.exifInfo!.latitude != null &&
 | 
			
		||||
              assetDetail.exifInfo!.longitude != null)
 | 
			
		||||
          ? Padding(
 | 
			
		||||
              padding: const EdgeInsets.symmetric(vertical: 16.0),
 | 
			
		||||
              child: Container(
 | 
			
		||||
@@ -25,12 +27,14 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
                ),
 | 
			
		||||
                child: FlutterMap(
 | 
			
		||||
                  options: MapOptions(
 | 
			
		||||
                    center: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
 | 
			
		||||
                    center: LatLng(assetDetail.exifInfo!.latitude!,
 | 
			
		||||
                        assetDetail.exifInfo!.longitude!),
 | 
			
		||||
                    zoom: 16.0,
 | 
			
		||||
                  ),
 | 
			
		||||
                  layers: [
 | 
			
		||||
                    TileLayerOptions(
 | 
			
		||||
                      urlTemplate: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
 | 
			
		||||
                      urlTemplate:
 | 
			
		||||
                          "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
 | 
			
		||||
                      subdomains: ['a', 'b', 'c'],
 | 
			
		||||
                      attributionBuilder: (_) {
 | 
			
		||||
                        return const Text(
 | 
			
		||||
@@ -43,8 +47,10 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
                      markers: [
 | 
			
		||||
                        Marker(
 | 
			
		||||
                          anchorPos: AnchorPos.align(AnchorAlign.top),
 | 
			
		||||
                          point: LatLng(assetDetail.exifInfo!.latitude!, assetDetail.exifInfo!.longitude!),
 | 
			
		||||
                          builder: (ctx) => const Image(image: AssetImage('assets/location-pin.png')),
 | 
			
		||||
                          point: LatLng(assetDetail.exifInfo!.latitude!,
 | 
			
		||||
                              assetDetail.exifInfo!.longitude!),
 | 
			
		||||
                          builder: (ctx) => const Image(
 | 
			
		||||
                              image: AssetImage('assets/location-pin.png')),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -56,10 +62,14 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _buildLocationText() {
 | 
			
		||||
      return (assetDetail.exifInfo!.city != null && assetDetail.exifInfo!.state != null)
 | 
			
		||||
      return (assetDetail.exifInfo!.city != null &&
 | 
			
		||||
              assetDetail.exifInfo!.state != null)
 | 
			
		||||
          ? Text(
 | 
			
		||||
              "${assetDetail.exifInfo!.city}, ${assetDetail.exifInfo!.state}",
 | 
			
		||||
              style: TextStyle(fontSize: 12, color: Colors.grey[200], fontWeight: FontWeight.bold),
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                  fontSize: 12,
 | 
			
		||||
                  color: Colors.grey[200],
 | 
			
		||||
                  fontWeight: FontWeight.bold),
 | 
			
		||||
            )
 | 
			
		||||
          : Container();
 | 
			
		||||
    }
 | 
			
		||||
@@ -131,7 +141,8 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
                        padding: const EdgeInsets.only(bottom: 8.0),
 | 
			
		||||
                        child: Text(
 | 
			
		||||
                          "DETAILS",
 | 
			
		||||
                          style: TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
			
		||||
                          style:
 | 
			
		||||
                              TextStyle(fontSize: 11, color: Colors.grey[400]),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                      ListTile(
 | 
			
		||||
@@ -158,7 +169,8 @@ class ExifBottomSheet extends ConsumerWidget {
 | 
			
		||||
                              leading: const Icon(Icons.camera),
 | 
			
		||||
                              title: Text(
 | 
			
		||||
                                "${assetDetail.exifInfo?.make} ${assetDetail.exifInfo?.model}",
 | 
			
		||||
                                style: const TextStyle(fontWeight: FontWeight.bold),
 | 
			
		||||
                                style: const TextStyle(
 | 
			
		||||
                                    fontWeight: FontWeight.bold),
 | 
			
		||||
                              ),
 | 
			
		||||
                              subtitle: Text(
 | 
			
		||||
                                  "ƒ/${assetDetail.exifInfo?.fNumber}   1/${(1 / assetDetail.exifInfo!.exposureTime!).toStringAsFixed(0)}   ${assetDetail.exifInfo?.focalLength}mm   ISO${assetDetail.exifInfo?.iso} "),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'package:cached_network_image/cached_network_image.dart';
 | 
			
		||||
import 'package:flutter/cupertino.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter/src/widgets/framework.dart';
 | 
			
		||||
import 'package:photo_view/photo_view.dart';
 | 
			
		||||
 | 
			
		||||
enum _RemoteImageStatus { empty, thumbnail, full }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import 'dart:developer';
 | 
			
		||||
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
@@ -5,7 +7,10 @@ import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
			
		||||
 | 
			
		||||
class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
  const TopControlAppBar(
 | 
			
		||||
      {Key? key, required this.asset, required this.onMoreInfoPressed, required this.onDownloadPressed})
 | 
			
		||||
      {Key? key,
 | 
			
		||||
      required this.asset,
 | 
			
		||||
      required this.onMoreInfoPressed,
 | 
			
		||||
      required this.onDownloadPressed})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  final ImmichAsset asset;
 | 
			
		||||
@@ -42,9 +47,11 @@ class TopControlAppBar extends ConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
          iconSize: iconSize,
 | 
			
		||||
          splashRadius: iconSize,
 | 
			
		||||
          onPressed: () {
 | 
			
		||||
            print("favorite");
 | 
			
		||||
            log("favorite");
 | 
			
		||||
          },
 | 
			
		||||
          icon: asset.isFavorite ? const Icon(Icons.favorite_rounded) : const Icon(Icons.favorite_border_rounded),
 | 
			
		||||
          icon: asset.isFavorite
 | 
			
		||||
              ? const Icon(Icons.favorite_rounded)
 | 
			
		||||
              : const Icon(Icons.favorite_border_rounded),
 | 
			
		||||
        ),
 | 
			
		||||
        IconButton(
 | 
			
		||||
            iconSize: iconSize,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,15 @@
 | 
			
		||||
import 'package:cancellation_token_http/http.dart';
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/server_info.service.dart';
 | 
			
		||||
import 'package:photo_manager/photo_manager.dart';
 | 
			
		||||
 | 
			
		||||
class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
@@ -55,7 +54,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
      removeExcludedAlbumForBackup(album);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
 | 
			
		||||
    state = state
 | 
			
		||||
        .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
 | 
			
		||||
    _updateBackupAssetCount();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -63,7 +63,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
    if (state.selectedBackupAlbums.contains(album)) {
 | 
			
		||||
      removeAlbumForBackup(album);
 | 
			
		||||
    }
 | 
			
		||||
    state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
 | 
			
		||||
    state = state
 | 
			
		||||
        .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
 | 
			
		||||
    _updateBackupAssetCount();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -94,16 +95,19 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
  Future<void> getBackupAlbumsInfo() async {
 | 
			
		||||
    // Get all albums on the device
 | 
			
		||||
    List<AvailableAlbum> availableAlbums = [];
 | 
			
		||||
    List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(hasAll: true, type: RequestType.common);
 | 
			
		||||
    List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
 | 
			
		||||
        hasAll: true, type: RequestType.common);
 | 
			
		||||
 | 
			
		||||
    for (AssetPathEntity album in albums) {
 | 
			
		||||
      AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
 | 
			
		||||
 | 
			
		||||
      var assetList = await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
			
		||||
      var assetList =
 | 
			
		||||
          await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
			
		||||
 | 
			
		||||
      if (assetList.isNotEmpty) {
 | 
			
		||||
        var thumbnailAsset = assetList.first;
 | 
			
		||||
        var thumbnailData = await thumbnailAsset.thumbnailDataWithSize(const ThumbnailSize(512, 512));
 | 
			
		||||
        var thumbnailData = await thumbnailAsset
 | 
			
		||||
            .thumbnailDataWithSize(const ThumbnailSize(512, 512));
 | 
			
		||||
        availableAlbum = availableAlbum.copyWith(thumbnailData: thumbnailData);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -114,7 +118,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
 | 
			
		||||
    // Put persistent storage info into local state of the app
 | 
			
		||||
    // Get local storage on selected backup album
 | 
			
		||||
    Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
			
		||||
    Box<HiveBackupAlbums> backupAlbumInfoBox =
 | 
			
		||||
        Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
			
		||||
    HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
 | 
			
		||||
      backupInfoKey,
 | 
			
		||||
      defaultValue: HiveBackupAlbums(
 | 
			
		||||
@@ -133,7 +138,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
      debugPrint("First time backup setup recent album as default");
 | 
			
		||||
 | 
			
		||||
      // Get album that contains all assets
 | 
			
		||||
      var list = await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.common);
 | 
			
		||||
      var list = await PhotoManager.getAssetPathList(
 | 
			
		||||
          hasAll: true, onlyAll: true, type: RequestType.common);
 | 
			
		||||
      AssetPathEntity albumHasAllAssets = list.first;
 | 
			
		||||
 | 
			
		||||
      backupAlbumInfoBox.put(
 | 
			
		||||
@@ -151,12 +157,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
    try {
 | 
			
		||||
      for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
 | 
			
		||||
        var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
 | 
			
		||||
        state = state.copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
 | 
			
		||||
        state = state.copyWith(
 | 
			
		||||
            selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset});
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
 | 
			
		||||
        var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
 | 
			
		||||
        state = state.copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
 | 
			
		||||
        state = state.copyWith(
 | 
			
		||||
            excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset});
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint("[ERROR] Failed to generate album from id $e");
 | 
			
		||||
@@ -173,21 +181,27 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
    Set<AssetEntity> assetsFromExcludedAlbums = {};
 | 
			
		||||
 | 
			
		||||
    for (var album in state.selectedBackupAlbums) {
 | 
			
		||||
      var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
			
		||||
      var assets =
 | 
			
		||||
          await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
			
		||||
      assetsFromSelectedAlbums.addAll(assets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (var album in state.excludedBackupAlbums) {
 | 
			
		||||
      var assets = await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
			
		||||
      var assets =
 | 
			
		||||
          await album.getAssetListRange(start: 0, end: album.assetCount);
 | 
			
		||||
      assetsFromExcludedAlbums.addAll(assets);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Set<AssetEntity> allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
 | 
			
		||||
    List<String> allAssetOnDatabase = await _backupService.getDeviceBackupAsset();
 | 
			
		||||
    Set<AssetEntity> allUniqueAssets =
 | 
			
		||||
        assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
 | 
			
		||||
    List<String> allAssetOnDatabase =
 | 
			
		||||
        await _backupService.getDeviceBackupAsset();
 | 
			
		||||
 | 
			
		||||
    // Find asset that were backup from selected albums
 | 
			
		||||
    Set<String> selectedAlbumsBackupAssets = Set.from(allUniqueAssets.map((e) => e.id));
 | 
			
		||||
    selectedAlbumsBackupAssets.removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
 | 
			
		||||
    Set<String> selectedAlbumsBackupAssets =
 | 
			
		||||
        Set.from(allUniqueAssets.map((e) => e.id));
 | 
			
		||||
    selectedAlbumsBackupAssets
 | 
			
		||||
        .removeWhere((assetId) => !allAssetOnDatabase.contains(assetId));
 | 
			
		||||
 | 
			
		||||
    if (allUniqueAssets.isEmpty) {
 | 
			
		||||
      debugPrint("No Asset On Device");
 | 
			
		||||
@@ -226,7 +240,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
  /// Hive database
 | 
			
		||||
  ///
 | 
			
		||||
  void _updatePersistentAlbumsSelection() {
 | 
			
		||||
    Box<HiveBackupAlbums> backupAlbumInfoBox = Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
			
		||||
    Box<HiveBackupAlbums> backupAlbumInfoBox =
 | 
			
		||||
        Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
 | 
			
		||||
    backupAlbumInfoBox.put(
 | 
			
		||||
      backupInfoKey,
 | 
			
		||||
      HiveBackupAlbums(
 | 
			
		||||
@@ -268,7 +283,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
 | 
			
		||||
      // Perform Backup
 | 
			
		||||
      state = state.copyWith(cancelToken: CancellationToken());
 | 
			
		||||
      _backupService.backupAsset(assetsWillBeBackup, state.cancelToken, _onAssetUploaded, _onUploadProgress);
 | 
			
		||||
      _backupService.backupAsset(assetsWillBeBackup, state.cancelToken,
 | 
			
		||||
          _onAssetUploaded, _onUploadProgress);
 | 
			
		||||
    } else {
 | 
			
		||||
      PhotoManager.openSetting();
 | 
			
		||||
    }
 | 
			
		||||
@@ -276,23 +292,32 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
 | 
			
		||||
  void cancelBackup() {
 | 
			
		||||
    state.cancelToken.cancel();
 | 
			
		||||
    state = state.copyWith(backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
        backupProgress: BackUpProgressEnum.idle, progressInPercentage: 0.0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onAssetUploaded(String deviceAssetId, String deviceId) {
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
        selectedAlbumsBackupAssetsIds: {...state.selectedAlbumsBackupAssetsIds, deviceAssetId},
 | 
			
		||||
        allAssetOnDatabase: [...state.allAssetOnDatabase, deviceAssetId]);
 | 
			
		||||
    state = state.copyWith(selectedAlbumsBackupAssetsIds: {
 | 
			
		||||
      ...state.selectedAlbumsBackupAssetsIds,
 | 
			
		||||
      deviceAssetId
 | 
			
		||||
    }, allAssetOnDatabase: [
 | 
			
		||||
      ...state.allAssetOnDatabase,
 | 
			
		||||
      deviceAssetId
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) {
 | 
			
		||||
      state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
 | 
			
		||||
    if (state.allUniqueAssets.length -
 | 
			
		||||
            state.selectedAlbumsBackupAssetsIds.length ==
 | 
			
		||||
        0) {
 | 
			
		||||
      state = state.copyWith(
 | 
			
		||||
          backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _updateServerInfo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _onUploadProgress(int sent, int total) {
 | 
			
		||||
    state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
 | 
			
		||||
    state = state.copyWith(
 | 
			
		||||
        progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _updateServerInfo() async {
 | 
			
		||||
@@ -326,7 +351,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Check if this device is enable backup by the user
 | 
			
		||||
      if ((authState.deviceInfo.deviceId == authState.deviceId) && authState.deviceInfo.isAutoBackup) {
 | 
			
		||||
      if ((authState.deviceInfo.deviceId == authState.deviceId) &&
 | 
			
		||||
          authState.deviceInfo.isAutoBackup) {
 | 
			
		||||
        // check if backup is alreayd in process - then return
 | 
			
		||||
        if (state.backupProgress == BackUpProgressEnum.inProgress) {
 | 
			
		||||
          debugPrint("[resumeBackup] Backup is already in progress - abort");
 | 
			
		||||
@@ -343,6 +369,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
 | 
			
		||||
final backupProvider =
 | 
			
		||||
    StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
 | 
			
		||||
  return BackupNotifier(ref: ref);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -17,16 +17,21 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    BackUpState backupState = ref.watch(backupProvider);
 | 
			
		||||
    AuthenticationState _authenticationState = ref.watch(authenticationProvider);
 | 
			
		||||
    bool shouldBackup =
 | 
			
		||||
        backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length == 0 ? false : true;
 | 
			
		||||
    AuthenticationState authenticationState = ref.watch(authenticationProvider);
 | 
			
		||||
    bool shouldBackup = backupState.allUniqueAssets.length -
 | 
			
		||||
                backupState.selectedAlbumsBackupAssetsIds.length ==
 | 
			
		||||
            0
 | 
			
		||||
        ? false
 | 
			
		||||
        : true;
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
      if (backupState.backupProgress != BackUpProgressEnum.inProgress) {
 | 
			
		||||
        ref.read(backupProvider.notifier).getBackupInfo();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      ref.watch(websocketProvider.notifier).stopListenToEvent('on_upload_success');
 | 
			
		||||
      ref
 | 
			
		||||
          .watch(websocketProvider.notifier)
 | 
			
		||||
          .stopListenToEvent('on_upload_success');
 | 
			
		||||
      return null;
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +53,8 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
                child: LinearPercentIndicator(
 | 
			
		||||
                  padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
 | 
			
		||||
                  padding:
 | 
			
		||||
                      const EdgeInsets.symmetric(horizontal: 0, vertical: 0),
 | 
			
		||||
                  barRadius: const Radius.circular(2),
 | 
			
		||||
                  lineHeight: 6.0,
 | 
			
		||||
                  percent: backupState.serverInfo.diskUsagePercentage / 100.0,
 | 
			
		||||
@@ -58,7 +64,8 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
              Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(top: 12.0),
 | 
			
		||||
                child: Text('${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                    '${backupState.serverInfo.diskUse} of ${backupState.serverInfo.diskSize} used'),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          ),
 | 
			
		||||
@@ -67,9 +74,11 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ListTile _buildBackupController() {
 | 
			
		||||
      var backUpOption = _authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
 | 
			
		||||
      var isAutoBackup = _authenticationState.deviceInfo.isAutoBackup;
 | 
			
		||||
      var backupBtnText = _authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
 | 
			
		||||
      var backUpOption =
 | 
			
		||||
          authenticationState.deviceInfo.isAutoBackup ? "on" : "off";
 | 
			
		||||
      var isAutoBackup = authenticationState.deviceInfo.isAutoBackup;
 | 
			
		||||
      var backupBtnText =
 | 
			
		||||
          authenticationState.deviceInfo.isAutoBackup ? "off" : "on";
 | 
			
		||||
      return ListTile(
 | 
			
		||||
        isThreeLine: true,
 | 
			
		||||
        leading: isAutoBackup
 | 
			
		||||
@@ -104,10 +113,15 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
                  ),
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    isAutoBackup
 | 
			
		||||
                        ? ref.watch(authenticationProvider.notifier).setAutoBackup(false)
 | 
			
		||||
                        : ref.watch(authenticationProvider.notifier).setAutoBackup(true);
 | 
			
		||||
                        ? ref
 | 
			
		||||
                            .watch(authenticationProvider.notifier)
 | 
			
		||||
                            .setAutoBackup(false)
 | 
			
		||||
                        : ref
 | 
			
		||||
                            .watch(authenticationProvider.notifier)
 | 
			
		||||
                            .setAutoBackup(true);
 | 
			
		||||
                  },
 | 
			
		||||
                  child: Text("Turn $backupBtnText Backup", style: const TextStyle(fontWeight: FontWeight.bold)),
 | 
			
		||||
                  child: Text("Turn $backupBtnText Backup",
 | 
			
		||||
                      style: const TextStyle(fontWeight: FontWeight.bold)),
 | 
			
		||||
                ),
 | 
			
		||||
              )
 | 
			
		||||
            ],
 | 
			
		||||
@@ -133,7 +147,10 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
          padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            text.trim().substring(0, text.length - 2),
 | 
			
		||||
            style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
                color: Theme.of(context).primaryColor,
 | 
			
		||||
                fontSize: 12,
 | 
			
		||||
                fontWeight: FontWeight.bold),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -141,7 +158,10 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
          padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            "None selected",
 | 
			
		||||
            style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 12, fontWeight: FontWeight.bold),
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
                color: Theme.of(context).primaryColor,
 | 
			
		||||
                fontSize: 12,
 | 
			
		||||
                fontWeight: FontWeight.bold),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
@@ -160,7 +180,10 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
          padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            text.trim().substring(0, text.length - 2),
 | 
			
		||||
            style: TextStyle(color: Colors.red[300], fontSize: 12, fontWeight: FontWeight.bold),
 | 
			
		||||
            style: TextStyle(
 | 
			
		||||
                color: Colors.red[300],
 | 
			
		||||
                fontSize: 12,
 | 
			
		||||
                fontWeight: FontWeight.bold),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -181,7 +204,8 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
        borderOnForeground: false,
 | 
			
		||||
        child: ListTile(
 | 
			
		||||
          minVerticalPadding: 15,
 | 
			
		||||
          title: const Text("Backup Albums", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
 | 
			
		||||
          title: const Text("Backup Albums",
 | 
			
		||||
              style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
 | 
			
		||||
          subtitle: Padding(
 | 
			
		||||
            padding: const EdgeInsets.only(top: 8.0),
 | 
			
		||||
            child: Column(
 | 
			
		||||
@@ -258,13 +282,16 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
            ),
 | 
			
		||||
            BackupInfoCard(
 | 
			
		||||
              title: "Backup",
 | 
			
		||||
              subtitle: "Photos and videos from selected albums that are backup",
 | 
			
		||||
              subtitle:
 | 
			
		||||
                  "Photos and videos from selected albums that are backup",
 | 
			
		||||
              info: "${backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
			
		||||
            ),
 | 
			
		||||
            BackupInfoCard(
 | 
			
		||||
              title: "Remainder",
 | 
			
		||||
              subtitle: "Photos and videos that has not been backing up from selected albums",
 | 
			
		||||
              info: "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
			
		||||
              subtitle:
 | 
			
		||||
                  "Photos and videos that has not been backing up from selected albums",
 | 
			
		||||
              info:
 | 
			
		||||
                  "${backupState.allUniqueAssets.length - backupState.selectedAlbumsBackupAssetsIds.length}",
 | 
			
		||||
            ),
 | 
			
		||||
            const Divider(),
 | 
			
		||||
            _buildBackupController(),
 | 
			
		||||
@@ -289,29 +316,32 @@ class BackupControllerPage extends HookConsumerWidget {
 | 
			
		||||
            Padding(
 | 
			
		||||
              padding: const EdgeInsets.all(8.0),
 | 
			
		||||
              child: Container(
 | 
			
		||||
                child: backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
			
		||||
                    ? ElevatedButton(
 | 
			
		||||
                        style: ElevatedButton.styleFrom(
 | 
			
		||||
                          primary: Colors.red[300],
 | 
			
		||||
                          onPrimary: Colors.grey[50],
 | 
			
		||||
                        ),
 | 
			
		||||
                        onPressed: () {
 | 
			
		||||
                          ref.read(backupProvider.notifier).cancelBackup();
 | 
			
		||||
                        },
 | 
			
		||||
                        child: const Text("Cancel"),
 | 
			
		||||
                      )
 | 
			
		||||
                    : ElevatedButton(
 | 
			
		||||
                        style: ElevatedButton.styleFrom(
 | 
			
		||||
                          primary: Theme.of(context).primaryColor,
 | 
			
		||||
                          onPrimary: Colors.grey[50],
 | 
			
		||||
                        ),
 | 
			
		||||
                        onPressed: shouldBackup
 | 
			
		||||
                            ? () {
 | 
			
		||||
                                ref.read(backupProvider.notifier).startBackupProcess();
 | 
			
		||||
                              }
 | 
			
		||||
                            : null,
 | 
			
		||||
                        child: const Text("Start Backup"),
 | 
			
		||||
                      ),
 | 
			
		||||
                child:
 | 
			
		||||
                    backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
			
		||||
                        ? ElevatedButton(
 | 
			
		||||
                            style: ElevatedButton.styleFrom(
 | 
			
		||||
                              primary: Colors.red[300],
 | 
			
		||||
                              onPrimary: Colors.grey[50],
 | 
			
		||||
                            ),
 | 
			
		||||
                            onPressed: () {
 | 
			
		||||
                              ref.read(backupProvider.notifier).cancelBackup();
 | 
			
		||||
                            },
 | 
			
		||||
                            child: const Text("Cancel"),
 | 
			
		||||
                          )
 | 
			
		||||
                        : ElevatedButton(
 | 
			
		||||
                            style: ElevatedButton.styleFrom(
 | 
			
		||||
                              primary: Theme.of(context).primaryColor,
 | 
			
		||||
                              onPrimary: Colors.grey[50],
 | 
			
		||||
                            ),
 | 
			
		||||
                            onPressed: shouldBackup
 | 
			
		||||
                                ? () {
 | 
			
		||||
                                    ref
 | 
			
		||||
                                        .read(backupProvider.notifier)
 | 
			
		||||
                                        .startBackupProcess();
 | 
			
		||||
                                  }
 | 
			
		||||
                                : null,
 | 
			
		||||
                            child: const Text("Start Backup"),
 | 
			
		||||
                          ),
 | 
			
		||||
              ),
 | 
			
		||||
            )
 | 
			
		||||
          ],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/providers/home_page_state.provider.dart';
 | 
			
		||||
 | 
			
		||||
class DisableMultiSelectButton extends ConsumerWidget {
 | 
			
		||||
  const DisableMultiSelectButton({
 | 
			
		||||
@@ -36,7 +35,8 @@ class DisableMultiSelectButton extends ConsumerWidget {
 | 
			
		||||
                  icon: const Icon(Icons.close_rounded),
 | 
			
		||||
                  label: Text(
 | 
			
		||||
                    selectedItemCount.toString(),
 | 
			
		||||
                    style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                        fontWeight: FontWeight.w600, fontSize: 18),
 | 
			
		||||
                  )),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,8 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    this.labelTextBuilder,
 | 
			
		||||
    this.labelConstraints,
 | 
			
		||||
  })  : assert(child.scrollDirection == Axis.vertical),
 | 
			
		||||
        scrollThumbBuilder = _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
 | 
			
		||||
        scrollThumbBuilder =
 | 
			
		||||
            _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
 | 
			
		||||
        super(key: key);
 | 
			
		||||
 | 
			
		||||
  DraggableScrollbar.arrows({
 | 
			
		||||
@@ -98,7 +99,8 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    this.labelTextBuilder,
 | 
			
		||||
    this.labelConstraints,
 | 
			
		||||
  })  : assert(child.scrollDirection == Axis.vertical),
 | 
			
		||||
        scrollThumbBuilder = _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
 | 
			
		||||
        scrollThumbBuilder =
 | 
			
		||||
            _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb),
 | 
			
		||||
        super(key: key);
 | 
			
		||||
 | 
			
		||||
  DraggableScrollbar.semicircle({
 | 
			
		||||
@@ -115,11 +117,12 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    this.labelTextBuilder,
 | 
			
		||||
    this.labelConstraints,
 | 
			
		||||
  })  : assert(child.scrollDirection == Axis.vertical),
 | 
			
		||||
        scrollThumbBuilder = _thumbSemicircleBuilder(heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
 | 
			
		||||
        scrollThumbBuilder = _thumbSemicircleBuilder(
 | 
			
		||||
            heightScrollThumb * 0.6, scrollThumbKey, alwaysVisibleScrollThumb),
 | 
			
		||||
        super(key: key);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  _DraggableScrollbarState createState() => _DraggableScrollbarState();
 | 
			
		||||
  DraggableScrollbarState createState() => DraggableScrollbarState();
 | 
			
		||||
 | 
			
		||||
  static buildScrollThumbAndLabel(
 | 
			
		||||
      {required Widget scrollThumb,
 | 
			
		||||
@@ -137,9 +140,9 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
            children: [
 | 
			
		||||
              ScrollLabel(
 | 
			
		||||
                animation: labelAnimation,
 | 
			
		||||
                child: labelText,
 | 
			
		||||
                backgroundColor: backgroundColor,
 | 
			
		||||
                constraints: labelConstraints,
 | 
			
		||||
                child: labelText,
 | 
			
		||||
              ),
 | 
			
		||||
              scrollThumb,
 | 
			
		||||
            ],
 | 
			
		||||
@@ -154,7 +157,8 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static ScrollThumbBuilder _thumbSemicircleBuilder(double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
 | 
			
		||||
  static ScrollThumbBuilder _thumbSemicircleBuilder(
 | 
			
		||||
      double width, Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
 | 
			
		||||
    return (
 | 
			
		||||
      Color backgroundColor,
 | 
			
		||||
      Animation<double> thumbAnimation,
 | 
			
		||||
@@ -168,9 +172,6 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
        foregroundPainter: ArrowCustomPainter(Colors.white),
 | 
			
		||||
        child: Material(
 | 
			
		||||
          elevation: 4.0,
 | 
			
		||||
          child: Container(
 | 
			
		||||
            constraints: BoxConstraints.tight(Size(width, height)),
 | 
			
		||||
          ),
 | 
			
		||||
          color: backgroundColor,
 | 
			
		||||
          borderRadius: BorderRadius.only(
 | 
			
		||||
            topLeft: Radius.circular(height),
 | 
			
		||||
@@ -178,6 +179,9 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
            topRight: const Radius.circular(4.0),
 | 
			
		||||
            bottomRight: const Radius.circular(4.0),
 | 
			
		||||
          ),
 | 
			
		||||
          child: Container(
 | 
			
		||||
            constraints: BoxConstraints.tight(Size(width, height)),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
@@ -193,7 +197,8 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static ScrollThumbBuilder _thumbArrowBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
 | 
			
		||||
  static ScrollThumbBuilder _thumbArrowBuilder(
 | 
			
		||||
      Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
 | 
			
		||||
    return (
 | 
			
		||||
      Color backgroundColor,
 | 
			
		||||
      Animation<double> thumbAnimation,
 | 
			
		||||
@@ -203,6 +208,7 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
      BoxConstraints? labelConstraints,
 | 
			
		||||
    }) {
 | 
			
		||||
      final scrollThumb = ClipPath(
 | 
			
		||||
        clipper: ArrowClipper(),
 | 
			
		||||
        child: Container(
 | 
			
		||||
          height: height,
 | 
			
		||||
          width: 20.0,
 | 
			
		||||
@@ -213,7 +219,6 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        clipper: ArrowClipper(),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return buildScrollThumbAndLabel(
 | 
			
		||||
@@ -228,7 +233,8 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static ScrollThumbBuilder _thumbRRectBuilder(Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
 | 
			
		||||
  static ScrollThumbBuilder _thumbRRectBuilder(
 | 
			
		||||
      Key? scrollThumbKey, bool alwaysVisibleScrollThumb) {
 | 
			
		||||
    return (
 | 
			
		||||
      Color backgroundColor,
 | 
			
		||||
      Animation<double> thumbAnimation,
 | 
			
		||||
@@ -239,13 +245,13 @@ class DraggableScrollbar extends StatefulWidget {
 | 
			
		||||
    }) {
 | 
			
		||||
      final scrollThumb = Material(
 | 
			
		||||
        elevation: 4.0,
 | 
			
		||||
        color: backgroundColor,
 | 
			
		||||
        borderRadius: const BorderRadius.all(Radius.circular(7.0)),
 | 
			
		||||
        child: Container(
 | 
			
		||||
          constraints: BoxConstraints.tight(
 | 
			
		||||
            Size(16.0, height),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
        color: backgroundColor,
 | 
			
		||||
        borderRadius: const BorderRadius.all(Radius.circular(7.0)),
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      return buildScrollThumbAndLabel(
 | 
			
		||||
@@ -267,7 +273,8 @@ class ScrollLabel extends StatelessWidget {
 | 
			
		||||
  final Text child;
 | 
			
		||||
 | 
			
		||||
  final BoxConstraints? constraints;
 | 
			
		||||
  static const BoxConstraints _defaultConstraints = BoxConstraints.tightFor(width: 72.0, height: 28.0);
 | 
			
		||||
  static const BoxConstraints _defaultConstraints =
 | 
			
		||||
      BoxConstraints.tightFor(width: 72.0, height: 28.0);
 | 
			
		||||
 | 
			
		||||
  const ScrollLabel({
 | 
			
		||||
    Key? key,
 | 
			
		||||
@@ -298,7 +305,8 @@ class ScrollLabel extends StatelessWidget {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProviderStateMixin {
 | 
			
		||||
class DraggableScrollbarState extends State<DraggableScrollbar>
 | 
			
		||||
    with TickerProviderStateMixin {
 | 
			
		||||
  late double _barOffset;
 | 
			
		||||
  late double _viewOffset;
 | 
			
		||||
  late bool _isDragInProcess;
 | 
			
		||||
@@ -345,7 +353,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
 | 
			
		||||
    super.dispose();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  double get barMaxScrollExtent => context.size!.height - widget.heightScrollThumb;
 | 
			
		||||
  double get barMaxScrollExtent =>
 | 
			
		||||
      context.size!.height - widget.heightScrollThumb;
 | 
			
		||||
 | 
			
		||||
  double get barMinScrollExtent => 0;
 | 
			
		||||
 | 
			
		||||
@@ -362,7 +371,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
 | 
			
		||||
    return LayoutBuilder(
 | 
			
		||||
        builder: (BuildContext context, BoxConstraints constraints) {
 | 
			
		||||
      //print("LayoutBuilder constraints=$constraints");
 | 
			
		||||
 | 
			
		||||
      return NotificationListener<ScrollNotification>(
 | 
			
		||||
@@ -432,7 +442,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (notification is ScrollUpdateNotification || notification is OverscrollNotification) {
 | 
			
		||||
      if (notification is ScrollUpdateNotification ||
 | 
			
		||||
          notification is OverscrollNotification) {
 | 
			
		||||
        if (_thumbAnimationController.status != AnimationStatus.forward) {
 | 
			
		||||
          _thumbAnimationController.forward();
 | 
			
		||||
        }
 | 
			
		||||
@@ -486,7 +497,8 @@ class _DraggableScrollbarState extends State<DraggableScrollbar> with TickerProv
 | 
			
		||||
          _barOffset = barMaxScrollExtent;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        double viewDelta = getScrollViewDelta(details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
 | 
			
		||||
        double viewDelta = getScrollViewDelta(
 | 
			
		||||
            details.delta.dy, barMaxScrollExtent, viewMaxScrollExtent);
 | 
			
		||||
 | 
			
		||||
        _viewOffset = widget.controller.position.pixels + viewDelta;
 | 
			
		||||
        if (_viewOffset < widget.controller.position.minScrollExtent) {
 | 
			
		||||
@@ -566,7 +578,8 @@ class ArrowClipper extends CustomClipper<Path> {
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2);
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth, startPointY);
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth, startPointY + 1.0);
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
 | 
			
		||||
    path.lineTo(
 | 
			
		||||
        startPointX + arrowWidth / 2, startPointY - arrowWidth / 2 + 1.0);
 | 
			
		||||
    path.lineTo(startPointX, startPointY + 1.0);
 | 
			
		||||
    path.close();
 | 
			
		||||
 | 
			
		||||
@@ -575,7 +588,8 @@ class ArrowClipper extends CustomClipper<Path> {
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2);
 | 
			
		||||
    path.lineTo(startPointX, startPointY);
 | 
			
		||||
    path.lineTo(startPointX, startPointY - 1.0);
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
 | 
			
		||||
    path.lineTo(
 | 
			
		||||
        startPointX + arrowWidth / 2, startPointY + arrowWidth / 2 - 1.0);
 | 
			
		||||
    path.lineTo(startPointX + arrowWidth, startPointY - 1.0);
 | 
			
		||||
    path.close();
 | 
			
		||||
 | 
			
		||||
@@ -600,7 +614,8 @@ class SlideFadeTransition extends StatelessWidget {
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return AnimatedBuilder(
 | 
			
		||||
      animation: animation,
 | 
			
		||||
      builder: (context, child) => animation.value == 0.0 ? Container() : child!,
 | 
			
		||||
      builder: (context, child) =>
 | 
			
		||||
          animation.value == 0.0 ? Container() : child!,
 | 
			
		||||
      child: SlideTransition(
 | 
			
		||||
        position: Tween(
 | 
			
		||||
          begin: const Offset(0.3, 0.0),
 | 
			
		||||
 
 | 
			
		||||
@@ -20,16 +20,18 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final BackUpState _backupState = ref.watch(backupProvider);
 | 
			
		||||
    bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
 | 
			
		||||
    final ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
 | 
			
		||||
    final BackUpState backupState = ref.watch(backupProvider);
 | 
			
		||||
    bool isEnableAutoBackup =
 | 
			
		||||
        ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
 | 
			
		||||
    final ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
 | 
			
		||||
 | 
			
		||||
    return SliverAppBar(
 | 
			
		||||
      centerTitle: true,
 | 
			
		||||
      floating: true,
 | 
			
		||||
      pinned: false,
 | 
			
		||||
      snap: false,
 | 
			
		||||
      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
 | 
			
		||||
      shape: const RoundedRectangleBorder(
 | 
			
		||||
          borderRadius: BorderRadius.all(Radius.circular(5))),
 | 
			
		||||
      leading: Builder(
 | 
			
		||||
        builder: (BuildContext context) {
 | 
			
		||||
          return Stack(
 | 
			
		||||
@@ -47,7 +49,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
                  },
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              _serverInfoState.isVersionMismatch
 | 
			
		||||
              serverInfoState.isVersionMismatch
 | 
			
		||||
                  ? Positioned(
 | 
			
		||||
                      bottom: 12,
 | 
			
		||||
                      right: 12,
 | 
			
		||||
@@ -88,7 +90,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
        Stack(
 | 
			
		||||
          alignment: AlignmentDirectional.center,
 | 
			
		||||
          children: [
 | 
			
		||||
            _backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
			
		||||
            backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
			
		||||
                ? Positioned(
 | 
			
		||||
                    top: 10,
 | 
			
		||||
                    right: 12,
 | 
			
		||||
@@ -97,7 +99,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
                      width: 8,
 | 
			
		||||
                      child: CircularProgressIndicator(
 | 
			
		||||
                        strokeWidth: 1,
 | 
			
		||||
                        valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
 | 
			
		||||
                        valueColor: AlwaysStoppedAnimation<Color>(
 | 
			
		||||
                            Theme.of(context).primaryColor),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
@@ -105,7 +108,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
            IconButton(
 | 
			
		||||
              splashRadius: 25,
 | 
			
		||||
              iconSize: 30,
 | 
			
		||||
              icon: _isEnableAutoBackup
 | 
			
		||||
              icon: isEnableAutoBackup
 | 
			
		||||
                  ? const Icon(Icons.backup_rounded)
 | 
			
		||||
                  : Badge(
 | 
			
		||||
                      padding: const EdgeInsets.all(4),
 | 
			
		||||
@@ -118,20 +121,23 @@ class ImmichSliverAppBar extends ConsumerWidget {
 | 
			
		||||
                      ),
 | 
			
		||||
                      child: const Icon(Icons.backup_rounded)),
 | 
			
		||||
              onPressed: () async {
 | 
			
		||||
                var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
 | 
			
		||||
                var onPop = await AutoRouter.of(context)
 | 
			
		||||
                    .push(const BackupControllerRoute());
 | 
			
		||||
 | 
			
		||||
                if (onPop != null && onPop == true) {
 | 
			
		||||
                  onPopBack!();
 | 
			
		||||
                }
 | 
			
		||||
              },
 | 
			
		||||
            ),
 | 
			
		||||
            _backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
			
		||||
            backupState.backupProgress == BackUpProgressEnum.inProgress
 | 
			
		||||
                ? Positioned(
 | 
			
		||||
                    bottom: 5,
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      (_backupState.allUniqueAssets.length - _backupState.selectedAlbumsBackupAssetsIds.length)
 | 
			
		||||
                      (backupState.allUniqueAssets.length -
 | 
			
		||||
                              backupState.selectedAlbumsBackupAssetsIds.length)
 | 
			
		||||
                          .toString(),
 | 
			
		||||
                      style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
 | 
			
		||||
                      style: const TextStyle(
 | 
			
		||||
                          fontSize: 9, fontWeight: FontWeight.bold),
 | 
			
		||||
                    ),
 | 
			
		||||
                  )
 | 
			
		||||
                : Container()
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,10 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    String endpoint = Hive.box(userInfoBox).get(serverEndpointKey);
 | 
			
		||||
    AuthenticationState _authState = ref.watch(authenticationProvider);
 | 
			
		||||
    ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
 | 
			
		||||
    final uploadProfileImageStatus = ref.watch(uploadProfileImageProvider).status;
 | 
			
		||||
    AuthenticationState authState = ref.watch(authenticationProvider);
 | 
			
		||||
    ServerInfoState serverInfoState = ref.watch(serverInfoProvider);
 | 
			
		||||
    final uploadProfileImageStatus =
 | 
			
		||||
        ref.watch(uploadProfileImageProvider).status;
 | 
			
		||||
    final appInfo = useState({});
 | 
			
		||||
    var dummmy = Random().nextInt(1024);
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +41,7 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _buildUserProfileImage() {
 | 
			
		||||
      if (_authState.profileImagePath.isEmpty) {
 | 
			
		||||
      if (authState.profileImagePath.isEmpty) {
 | 
			
		||||
        return const CircleAvatar(
 | 
			
		||||
          radius: 35,
 | 
			
		||||
          backgroundImage: AssetImage('assets/immich-logo-no-outline.png'),
 | 
			
		||||
@@ -49,10 +50,11 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (uploadProfileImageStatus == UploadProfileStatus.idle) {
 | 
			
		||||
        if (_authState.profileImagePath.isNotEmpty) {
 | 
			
		||||
        if (authState.profileImagePath.isNotEmpty) {
 | 
			
		||||
          return CircleAvatar(
 | 
			
		||||
            radius: 35,
 | 
			
		||||
            backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
 | 
			
		||||
            backgroundImage: NetworkImage(
 | 
			
		||||
                '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
 | 
			
		||||
            backgroundColor: Colors.transparent,
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -67,7 +69,8 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
      if (uploadProfileImageStatus == UploadProfileStatus.success) {
 | 
			
		||||
        return CircleAvatar(
 | 
			
		||||
          radius: 35,
 | 
			
		||||
          backgroundImage: NetworkImage('$endpoint/user/profile-image/${_authState.userId}?d=${dummmy++}'),
 | 
			
		||||
          backgroundImage: NetworkImage(
 | 
			
		||||
              '$endpoint/user/profile-image/${authState.userId}?d=${dummmy++}'),
 | 
			
		||||
          backgroundColor: Colors.transparent,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
@@ -88,15 +91,16 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _pickUserProfileImage() async {
 | 
			
		||||
      final XFile? image = await ImagePicker().pickImage(source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
 | 
			
		||||
      final XFile? image = await ImagePicker().pickImage(
 | 
			
		||||
          source: ImageSource.gallery, maxHeight: 1024, maxWidth: 1024);
 | 
			
		||||
 | 
			
		||||
      if (image != null) {
 | 
			
		||||
        var success = await ref.watch(uploadProfileImageProvider.notifier).upload(image);
 | 
			
		||||
        var success =
 | 
			
		||||
            await ref.watch(uploadProfileImageProvider.notifier).upload(image);
 | 
			
		||||
 | 
			
		||||
        if (success) {
 | 
			
		||||
          ref
 | 
			
		||||
              .watch(authenticationProvider.notifier)
 | 
			
		||||
              .updateUserProfileImagePath(ref.read(uploadProfileImageProvider).profileImagePath);
 | 
			
		||||
          ref.watch(authenticationProvider.notifier).updateUserProfileImagePath(
 | 
			
		||||
              ref.read(uploadProfileImageProvider).profileImagePath);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -117,7 +121,10 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
              DrawerHeader(
 | 
			
		||||
                decoration: const BoxDecoration(
 | 
			
		||||
                  gradient: LinearGradient(
 | 
			
		||||
                    colors: [Color.fromARGB(255, 216, 219, 238), Color.fromARGB(255, 226, 230, 231)],
 | 
			
		||||
                    colors: [
 | 
			
		||||
                      Color.fromARGB(255, 216, 219, 238),
 | 
			
		||||
                      Color.fromARGB(255, 226, 230, 231)
 | 
			
		||||
                    ],
 | 
			
		||||
                    begin: Alignment.centerRight,
 | 
			
		||||
                    end: Alignment.centerLeft,
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -155,7 +162,7 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                      ],
 | 
			
		||||
                    ),
 | 
			
		||||
                    Text(
 | 
			
		||||
                      "${_authState.firstName} ${_authState.lastName}",
 | 
			
		||||
                      "${authState.firstName} ${authState.lastName}",
 | 
			
		||||
                      style: TextStyle(
 | 
			
		||||
                        color: Theme.of(context).primaryColor,
 | 
			
		||||
                        fontWeight: FontWeight.bold,
 | 
			
		||||
@@ -163,7 +170,7 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    Text(
 | 
			
		||||
                      _authState.userEmail,
 | 
			
		||||
                      authState.userEmail,
 | 
			
		||||
                      style: TextStyle(color: Colors.grey[800], fontSize: 12),
 | 
			
		||||
                    )
 | 
			
		||||
                  ],
 | 
			
		||||
@@ -177,10 +184,14 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                ),
 | 
			
		||||
                title: const Text(
 | 
			
		||||
                  "Sign Out",
 | 
			
		||||
                  style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                      color: Colors.black54,
 | 
			
		||||
                      fontSize: 14,
 | 
			
		||||
                      fontWeight: FontWeight.bold),
 | 
			
		||||
                ),
 | 
			
		||||
                onTap: () async {
 | 
			
		||||
                  bool res = await ref.read(authenticationProvider.notifier).logout();
 | 
			
		||||
                  bool res =
 | 
			
		||||
                      await ref.read(authenticationProvider.notifier).logout();
 | 
			
		||||
 | 
			
		||||
                  if (res) {
 | 
			
		||||
                    ref.watch(backupProvider.notifier).cancelBackup();
 | 
			
		||||
@@ -206,19 +217,22 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              child: Padding(
 | 
			
		||||
                padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
 | 
			
		||||
                padding:
 | 
			
		||||
                    const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
 | 
			
		||||
                child: Column(
 | 
			
		||||
                  crossAxisAlignment: CrossAxisAlignment.center,
 | 
			
		||||
                  children: [
 | 
			
		||||
                    Padding(
 | 
			
		||||
                      padding: const EdgeInsets.all(8.0),
 | 
			
		||||
                      child: Text(
 | 
			
		||||
                        _serverInfoState.isVersionMismatch
 | 
			
		||||
                            ? _serverInfoState.versionMismatchErrorMessage
 | 
			
		||||
                        serverInfoState.isVersionMismatch
 | 
			
		||||
                            ? serverInfoState.versionMismatchErrorMessage
 | 
			
		||||
                            : "Client and Server are up-to-date",
 | 
			
		||||
                        textAlign: TextAlign.center,
 | 
			
		||||
                        style:
 | 
			
		||||
                            TextStyle(fontSize: 11, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w600),
 | 
			
		||||
                        style: TextStyle(
 | 
			
		||||
                            fontSize: 11,
 | 
			
		||||
                            color: Theme.of(context).primaryColor,
 | 
			
		||||
                            fontWeight: FontWeight.w600),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    const Divider(),
 | 
			
		||||
@@ -256,7 +270,7 @@ class ProfileDrawer extends HookConsumerWidget {
 | 
			
		||||
                          ),
 | 
			
		||||
                        ),
 | 
			
		||||
                        Text(
 | 
			
		||||
                          "${_serverInfoState.serverVersion.major}.${_serverInfoState.serverVersion.minor}.${_serverInfoState.serverVersion.patch}",
 | 
			
		||||
                          "${serverInfoState.serverVersion.major}.${serverInfoState.serverVersion.minor}.${serverInfoState.serverVersion.patch}",
 | 
			
		||||
                          style: TextStyle(
 | 
			
		||||
                            fontSize: 11,
 | 
			
		||||
                            color: Colors.grey[500],
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,11 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    ScrollController _scrollController = useScrollController();
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
    var assetGroupByDateTime = ref.watch(assetGroupByDateTimeProvider);
 | 
			
		||||
    List<Widget> _imageGridGroup = [];
 | 
			
		||||
    var isMultiSelectEnable = ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    List<Widget> imageGridGroup = [];
 | 
			
		||||
    var isMultiSelectEnable =
 | 
			
		||||
        ref.watch(homePageStateProvider).isMultiSelectEnable;
 | 
			
		||||
    var homePageState = ref.watch(homePageStateProvider);
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
@@ -39,7 +40,8 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
    _buildSelectedItemCountIndicator() {
 | 
			
		||||
      return isMultiSelectEnable
 | 
			
		||||
          ? DisableMultiSelectButton(
 | 
			
		||||
              onPressed: ref.watch(homePageStateProvider.notifier).disableMultiSelect,
 | 
			
		||||
              onPressed:
 | 
			
		||||
                  ref.watch(homePageStateProvider.notifier).disableMultiSelect,
 | 
			
		||||
              selectedItemCount: homePageState.selectedItems.length,
 | 
			
		||||
            )
 | 
			
		||||
          : Container();
 | 
			
		||||
@@ -59,7 +61,7 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
          if (lastMonth != null) {
 | 
			
		||||
            if (currentMonth - lastMonth! != 0) {
 | 
			
		||||
              _imageGridGroup.add(
 | 
			
		||||
              imageGridGroup.add(
 | 
			
		||||
                MonthlyTitleText(
 | 
			
		||||
                  isoDate: dateGroup,
 | 
			
		||||
                ),
 | 
			
		||||
@@ -67,14 +69,14 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          _imageGridGroup.add(
 | 
			
		||||
          imageGridGroup.add(
 | 
			
		||||
            DailyTitleText(
 | 
			
		||||
              isoDate: dateGroup,
 | 
			
		||||
              assetGroup: immichAssetList,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          _imageGridGroup.add(
 | 
			
		||||
          imageGridGroup.add(
 | 
			
		||||
            ImageGrid(assetGroup: immichAssetList),
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
@@ -109,12 +111,12 @@ class HomePage extends HookConsumerWidget {
 | 
			
		||||
              padding: const EdgeInsets.only(top: 50.0),
 | 
			
		||||
              child: DraggableScrollbar.semicircle(
 | 
			
		||||
                backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
                controller: _scrollController,
 | 
			
		||||
                controller: scrollController,
 | 
			
		||||
                heightScrollThumb: 48.0,
 | 
			
		||||
                child: CustomScrollView(
 | 
			
		||||
                  controller: _scrollController,
 | 
			
		||||
                  controller: scrollController,
 | 
			
		||||
                  slivers: [
 | 
			
		||||
                    ..._imageGridGroup,
 | 
			
		||||
                    ...imageGridGroup,
 | 
			
		||||
                  ],
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
 
 | 
			
		||||
@@ -45,20 +45,23 @@ class SearchPageStateNotifier extends StateNotifier<SearchPageState> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void getSuggestedSearchTerms() async {
 | 
			
		||||
    var userSuggestedSearchTerms = await _searchService.getUserSuggestedSearchTerms();
 | 
			
		||||
    var userSuggestedSearchTerms =
 | 
			
		||||
        await _searchService.getUserSuggestedSearchTerms();
 | 
			
		||||
 | 
			
		||||
    state = state.copyWith(userSuggestedSearchTerms: userSuggestedSearchTerms);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final searchPageStateProvider = StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
 | 
			
		||||
final searchPageStateProvider =
 | 
			
		||||
    StateNotifierProvider<SearchPageStateNotifier, SearchPageState>((ref) {
 | 
			
		||||
  return SearchPageStateNotifier();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
 | 
			
		||||
  final SearchService _searchService = SearchService();
 | 
			
		||||
final getCuratedLocationProvider =
 | 
			
		||||
    FutureProvider.autoDispose<List<CuratedLocation>>((ref) async {
 | 
			
		||||
  final SearchService searchService = SearchService();
 | 
			
		||||
 | 
			
		||||
  var curatedLocation = await _searchService.getCuratedLocation();
 | 
			
		||||
  var curatedLocation = await searchService.getCuratedLocation();
 | 
			
		||||
  if (curatedLocation != null) {
 | 
			
		||||
    return curatedLocation;
 | 
			
		||||
  } else {
 | 
			
		||||
@@ -66,10 +69,11 @@ final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocati
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final getCuratedObjectProvider = FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
 | 
			
		||||
  final SearchService _searchService = SearchService();
 | 
			
		||||
final getCuratedObjectProvider =
 | 
			
		||||
    FutureProvider.autoDispose<List<CuratedObject>>((ref) async {
 | 
			
		||||
  final SearchService searchService = SearchService();
 | 
			
		||||
 | 
			
		||||
  var curatedObject = await _searchService.getCuratedObjects();
 | 
			
		||||
  var curatedObject = await searchService.getCuratedObjects();
 | 
			
		||||
  if (curatedObject != null) {
 | 
			
		||||
    return curatedObject;
 | 
			
		||||
  } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import 'package:auto_route/auto_route.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
 
 | 
			
		||||
@@ -12,24 +12,27 @@ import 'package:immich_mobile/modules/search/providers/search_result_page.provid
 | 
			
		||||
import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart';
 | 
			
		||||
 | 
			
		||||
class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
  SearchResultPage({Key? key, required this.searchTerm}) : super(key: key);
 | 
			
		||||
  const SearchResultPage({Key? key, required this.searchTerm})
 | 
			
		||||
      : super(key: key);
 | 
			
		||||
 | 
			
		||||
  final String searchTerm;
 | 
			
		||||
  late FocusNode searchFocusNode;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    ScrollController _scrollController = useScrollController();
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
    final searchTermController = useTextEditingController(text: "");
 | 
			
		||||
    final isNewSearch = useState(false);
 | 
			
		||||
    final currentSearchTerm = useState(searchTerm);
 | 
			
		||||
 | 
			
		||||
    List<Widget> _imageGridGroup = [];
 | 
			
		||||
    final List<Widget> imageGridGroup = [];
 | 
			
		||||
 | 
			
		||||
    late FocusNode searchFocusNode;
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
      searchFocusNode = FocusNode();
 | 
			
		||||
 | 
			
		||||
      Future.delayed(Duration.zero, () => ref.read(searchResultPageProvider.notifier).search(searchTerm));
 | 
			
		||||
      Future.delayed(Duration.zero,
 | 
			
		||||
          () => ref.read(searchResultPageProvider.notifier).search(searchTerm));
 | 
			
		||||
      return () => searchFocusNode.dispose();
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
@@ -85,7 +88,10 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
          children: [
 | 
			
		||||
            Text(
 | 
			
		||||
              currentSearchTerm.value,
 | 
			
		||||
              style: TextStyle(color: Theme.of(context).primaryColor, fontSize: 13, fontWeight: FontWeight.bold),
 | 
			
		||||
              style: TextStyle(
 | 
			
		||||
                  color: Theme.of(context).primaryColor,
 | 
			
		||||
                  fontSize: 13,
 | 
			
		||||
                  fontWeight: FontWeight.bold),
 | 
			
		||||
              maxLines: 1,
 | 
			
		||||
            ),
 | 
			
		||||
            Icon(
 | 
			
		||||
@@ -124,7 +130,7 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
            if (lastMonth != null) {
 | 
			
		||||
              if (currentMonth - lastMonth! != 0) {
 | 
			
		||||
                _imageGridGroup.add(
 | 
			
		||||
                imageGridGroup.add(
 | 
			
		||||
                  MonthlyTitleText(
 | 
			
		||||
                    isoDate: dateGroup,
 | 
			
		||||
                  ),
 | 
			
		||||
@@ -132,14 +138,14 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _imageGridGroup.add(
 | 
			
		||||
            imageGridGroup.add(
 | 
			
		||||
              DailyTitleText(
 | 
			
		||||
                isoDate: dateGroup,
 | 
			
		||||
                assetGroup: immichAssetList,
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            _imageGridGroup.add(
 | 
			
		||||
            imageGridGroup.add(
 | 
			
		||||
              ImageGrid(assetGroup: immichAssetList),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@@ -148,11 +154,11 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
          return DraggableScrollbar.semicircle(
 | 
			
		||||
            backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
            controller: _scrollController,
 | 
			
		||||
            controller: scrollController,
 | 
			
		||||
            heightScrollThumb: 48.0,
 | 
			
		||||
            child: CustomScrollView(
 | 
			
		||||
              controller: _scrollController,
 | 
			
		||||
              slivers: [..._imageGridGroup],
 | 
			
		||||
              controller: scrollController,
 | 
			
		||||
              slivers: [...imageGridGroup],
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -192,7 +198,9 @@ class SearchResultPage extends HookConsumerWidget {
 | 
			
		||||
        child: Stack(
 | 
			
		||||
          children: [
 | 
			
		||||
            _buildSearchResult(),
 | 
			
		||||
            isNewSearch.value ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(),
 | 
			
		||||
            isNewSearch.value
 | 
			
		||||
                ? SearchSuggestionList(onSubmitted: _onSearchSubmitted)
 | 
			
		||||
                : Container(),
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
 | 
			
		||||
  final SharedAlbumService _sharedAlbumService = SharedAlbumService();
 | 
			
		||||
 | 
			
		||||
  getAllSharedAlbums() async {
 | 
			
		||||
    List<SharedAlbum> sharedAlbums = await _sharedAlbumService.getAllSharedAlbum();
 | 
			
		||||
    List<SharedAlbum> sharedAlbums =
 | 
			
		||||
        await _sharedAlbumService.getAllSharedAlbum();
 | 
			
		||||
 | 
			
		||||
    state = sharedAlbums;
 | 
			
		||||
  }
 | 
			
		||||
@@ -35,7 +36,8 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> removeAssetFromAlbum(String albumId, List<String> assetIds) async {
 | 
			
		||||
  Future<bool> removeAssetFromAlbum(
 | 
			
		||||
      String albumId, List<String> assetIds) async {
 | 
			
		||||
    var res = await _sharedAlbumService.removeAssetFromAlbum(albumId, assetIds);
 | 
			
		||||
 | 
			
		||||
    if (res) {
 | 
			
		||||
@@ -46,12 +48,14 @@ class SharedAlbumNotifier extends StateNotifier<List<SharedAlbum>> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final sharedAlbumProvider = StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
 | 
			
		||||
final sharedAlbumProvider =
 | 
			
		||||
    StateNotifierProvider<SharedAlbumNotifier, List<SharedAlbum>>((ref) {
 | 
			
		||||
  return SharedAlbumNotifier();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose.family<SharedAlbum, String>((ref, albumId) async {
 | 
			
		||||
  final SharedAlbumService _sharedAlbumService = SharedAlbumService();
 | 
			
		||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose
 | 
			
		||||
    .family<SharedAlbum, String>((ref, albumId) async {
 | 
			
		||||
  final SharedAlbumService sharedAlbumService = SharedAlbumService();
 | 
			
		||||
 | 
			
		||||
  return await _sharedAlbumService.getAlbumDetail(albumId);
 | 
			
		||||
  return await sharedAlbumService.getAlbumDetail(albumId);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -26,18 +26,22 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final isMultiSelectionEnable = ref.watch(assetSelectionProvider).isMultiselectEnable;
 | 
			
		||||
    final selectedAssetsInAlbum = ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
			
		||||
    final isMultiSelectionEnable =
 | 
			
		||||
        ref.watch(assetSelectionProvider).isMultiselectEnable;
 | 
			
		||||
    final selectedAssetsInAlbum =
 | 
			
		||||
        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
 | 
			
		||||
    final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
 | 
			
		||||
    final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
 | 
			
		||||
 | 
			
		||||
    void _onDeleteAlbumPressed(String albumId) async {
 | 
			
		||||
      ImmichLoadingOverlayController.appLoader.show();
 | 
			
		||||
 | 
			
		||||
      bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
 | 
			
		||||
      bool isSuccess =
 | 
			
		||||
          await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(albumId);
 | 
			
		||||
 | 
			
		||||
      if (isSuccess) {
 | 
			
		||||
        AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
        AutoRouter.of(context)
 | 
			
		||||
            .navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
      } else {
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
          context: context,
 | 
			
		||||
@@ -53,10 +57,12 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
    void _onLeaveAlbumPressed(String albumId) async {
 | 
			
		||||
      ImmichLoadingOverlayController.appLoader.show();
 | 
			
		||||
 | 
			
		||||
      bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
 | 
			
		||||
      bool isSuccess =
 | 
			
		||||
          await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(albumId);
 | 
			
		||||
 | 
			
		||||
      if (isSuccess) {
 | 
			
		||||
        AutoRouter.of(context).navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
        AutoRouter.of(context)
 | 
			
		||||
            .navigate(const TabControllerRoute(children: [SharingRoute()]));
 | 
			
		||||
      } else {
 | 
			
		||||
        Navigator.pop(context);
 | 
			
		||||
        ImmichToast.show(
 | 
			
		||||
@@ -73,10 +79,11 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
    void _onRemoveFromAlbumPressed(String albumId) async {
 | 
			
		||||
      ImmichLoadingOverlayController.appLoader.show();
 | 
			
		||||
 | 
			
		||||
      bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
 | 
			
		||||
            albumId,
 | 
			
		||||
            selectedAssetsInAlbum.map((a) => a.id).toList(),
 | 
			
		||||
          );
 | 
			
		||||
      bool isSuccess =
 | 
			
		||||
          await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
 | 
			
		||||
                albumId,
 | 
			
		||||
                selectedAssetsInAlbum.map((a) => a.id).toList(),
 | 
			
		||||
              );
 | 
			
		||||
 | 
			
		||||
      if (isSuccess) {
 | 
			
		||||
        Navigator.pop(context);
 | 
			
		||||
@@ -153,15 +160,18 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
    _buildLeadingButton() {
 | 
			
		||||
      if (isMultiSelectionEnable) {
 | 
			
		||||
        return IconButton(
 | 
			
		||||
          onPressed: () => ref.watch(assetSelectionProvider.notifier).disableMultiselection(),
 | 
			
		||||
          onPressed: () => ref
 | 
			
		||||
              .watch(assetSelectionProvider.notifier)
 | 
			
		||||
              .disableMultiselection(),
 | 
			
		||||
          icon: const Icon(Icons.close_rounded),
 | 
			
		||||
          splashRadius: 25,
 | 
			
		||||
        );
 | 
			
		||||
      } else if (isEditAlbum) {
 | 
			
		||||
        return IconButton(
 | 
			
		||||
          onPressed: () async {
 | 
			
		||||
            bool isSuccess =
 | 
			
		||||
                await ref.watch(albumViewerProvider.notifier).changeAlbumTitle(albumId, userId, newAlbumTitle);
 | 
			
		||||
            bool isSuccess = await ref
 | 
			
		||||
                .watch(albumViewerProvider.notifier)
 | 
			
		||||
                .changeAlbumTitle(albumId, userId, newAlbumTitle);
 | 
			
		||||
 | 
			
		||||
            if (!isSuccess) {
 | 
			
		||||
              ImmichToast.show(
 | 
			
		||||
@@ -187,7 +197,9 @@ class AlbumViewerAppbar extends HookConsumerWidget with PreferredSizeWidget {
 | 
			
		||||
    return AppBar(
 | 
			
		||||
      elevation: 0,
 | 
			
		||||
      leading: _buildLeadingButton(),
 | 
			
		||||
      title: isMultiSelectionEnable ? Text(selectedAssetsInAlbum.length.toString()) : Container(),
 | 
			
		||||
      title: isMultiSelectionEnable
 | 
			
		||||
          ? Text(selectedAssetsInAlbum.length.toString())
 | 
			
		||||
          : Container(),
 | 
			
		||||
      centerTitle: false,
 | 
			
		||||
      actions: [
 | 
			
		||||
        IconButton(
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,9 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    FocusNode titleFocusNode = useFocusNode();
 | 
			
		||||
    ScrollController _scrollController = useScrollController();
 | 
			
		||||
    AsyncValue<SharedAlbum> _albumInfo = ref.watch(sharedAlbumDetailProvider(albumId));
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
    AsyncValue<SharedAlbum> albumInfo =
 | 
			
		||||
        ref.watch(sharedAlbumDetailProvider(albumId));
 | 
			
		||||
 | 
			
		||||
    final userId = ref.watch(authenticationProvider).userId;
 | 
			
		||||
 | 
			
		||||
@@ -44,16 +45,16 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
      ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
 | 
			
		||||
 | 
			
		||||
      AssetSelectionPageResult? returnPayload =
 | 
			
		||||
          await AutoRouter.of(context).push<AssetSelectionPageResult?>(const AssetSelectionRoute());
 | 
			
		||||
      AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
 | 
			
		||||
          .push<AssetSelectionPageResult?>(const AssetSelectionRoute());
 | 
			
		||||
 | 
			
		||||
      if (returnPayload != null) {
 | 
			
		||||
        // Check if there is new assets add
 | 
			
		||||
        if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
 | 
			
		||||
          ImmichLoadingOverlayController.appLoader.show();
 | 
			
		||||
 | 
			
		||||
          var isSuccess =
 | 
			
		||||
              await SharedAlbumService().addAdditionalAssetToAlbum(returnPayload.selectedAdditionalAsset, albumId);
 | 
			
		||||
          var isSuccess = await SharedAlbumService().addAdditionalAssetToAlbum(
 | 
			
		||||
              returnPayload.selectedAdditionalAsset, albumId);
 | 
			
		||||
 | 
			
		||||
          if (isSuccess) {
 | 
			
		||||
            ref.refresh(sharedAlbumDetailProvider(albumId));
 | 
			
		||||
@@ -69,13 +70,15 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void _onAddUsersPressed(SharedAlbum albumInfo) async {
 | 
			
		||||
      List<String>? sharedUserIds =
 | 
			
		||||
          await AutoRouter.of(context).push<List<String>?>(SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
 | 
			
		||||
      List<String>? sharedUserIds = await AutoRouter.of(context)
 | 
			
		||||
          .push<List<String>?>(
 | 
			
		||||
              SelectAdditionalUserForSharingRoute(albumInfo: albumInfo));
 | 
			
		||||
 | 
			
		||||
      if (sharedUserIds != null) {
 | 
			
		||||
        ImmichLoadingOverlayController.appLoader.show();
 | 
			
		||||
 | 
			
		||||
        var isSuccess = await SharedAlbumService().addAdditionalUserToAlbum(sharedUserIds, albumId);
 | 
			
		||||
        var isSuccess = await SharedAlbumService()
 | 
			
		||||
            .addAdditionalUserToAlbum(sharedUserIds, albumId);
 | 
			
		||||
 | 
			
		||||
        if (isSuccess) {
 | 
			
		||||
          ref.refresh(sharedAlbumDetailProvider(albumId));
 | 
			
		||||
@@ -95,7 +98,9 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
              )
 | 
			
		||||
            : Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(left: 8.0),
 | 
			
		||||
                child: Text(albumInfo.albumName, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
 | 
			
		||||
                child: Text(albumInfo.albumName,
 | 
			
		||||
                    style: const TextStyle(
 | 
			
		||||
                        fontSize: 24, fontWeight: FontWeight.bold)),
 | 
			
		||||
              ),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
@@ -103,8 +108,10 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
    Widget _buildAlbumDateRange(SharedAlbum albumInfo) {
 | 
			
		||||
      if (albumInfo.assets != null && albumInfo.assets!.isNotEmpty) {
 | 
			
		||||
        String startDate = "";
 | 
			
		||||
        DateTime parsedStartDate = DateTime.parse(albumInfo.assets!.first.createdAt);
 | 
			
		||||
        DateTime parsedEndDate = DateTime.parse(albumInfo.assets!.last.createdAt);
 | 
			
		||||
        DateTime parsedStartDate =
 | 
			
		||||
            DateTime.parse(albumInfo.assets!.first.createdAt);
 | 
			
		||||
        DateTime parsedEndDate =
 | 
			
		||||
            DateTime.parse(albumInfo.assets!.last.createdAt);
 | 
			
		||||
 | 
			
		||||
        if (parsedStartDate.year == parsedEndDate.year) {
 | 
			
		||||
          startDate = DateFormat('LLL d').format(parsedStartDate);
 | 
			
		||||
@@ -118,7 +125,8 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
          padding: const EdgeInsets.only(left: 16.0, top: 8),
 | 
			
		||||
          child: Text(
 | 
			
		||||
            "$startDate-$endDate",
 | 
			
		||||
            style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
 | 
			
		||||
            style: const TextStyle(
 | 
			
		||||
                fontSize: 14, fontWeight: FontWeight.bold, color: Colors.grey),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -147,8 +155,9 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
                      child: Padding(
 | 
			
		||||
                        padding: const EdgeInsets.all(2.0),
 | 
			
		||||
                        child: ClipRRect(
 | 
			
		||||
                          child: Image.asset('assets/immich-logo-no-outline.png'),
 | 
			
		||||
                          borderRadius: BorderRadius.circular(50.0),
 | 
			
		||||
                          child:
 | 
			
		||||
                              Image.asset('assets/immich-logo-no-outline.png'),
 | 
			
		||||
                        ),
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -217,10 +226,10 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
        },
 | 
			
		||||
        child: DraggableScrollbar.semicircle(
 | 
			
		||||
          backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
          controller: _scrollController,
 | 
			
		||||
          controller: scrollController,
 | 
			
		||||
          heightScrollThumb: 48.0,
 | 
			
		||||
          child: CustomScrollView(
 | 
			
		||||
            controller: _scrollController,
 | 
			
		||||
            controller: scrollController,
 | 
			
		||||
            slivers: [
 | 
			
		||||
              _buildHeader(albumInfo),
 | 
			
		||||
              SliverPersistentHeader(
 | 
			
		||||
@@ -242,8 +251,9 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Scaffold(
 | 
			
		||||
      appBar: AlbumViewerAppbar(albumInfo: _albumInfo, userId: userId, albumId: albumId),
 | 
			
		||||
      body: _albumInfo.when(
 | 
			
		||||
      appBar: AlbumViewerAppbar(
 | 
			
		||||
          albumInfo: albumInfo, userId: userId, albumId: albumId),
 | 
			
		||||
      body: albumInfo.when(
 | 
			
		||||
        data: (albumInfo) => _buildBody(albumInfo),
 | 
			
		||||
        error: (e, _) => Center(child: Text("Error loading album info $e")),
 | 
			
		||||
        loading: () => const Center(
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,15 @@ class AssetSelectionPage extends HookConsumerWidget {
 | 
			
		||||
  const AssetSelectionPage({Key? key}) : super(key: key);
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    ScrollController _scrollController = useScrollController();
 | 
			
		||||
    ScrollController scrollController = useScrollController();
 | 
			
		||||
    var assetGroupMonthYear = ref.watch(assetGroupByMonthYearProvider);
 | 
			
		||||
    final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
			
		||||
    final newAssetsForAlbum = ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
 | 
			
		||||
    final selectedAssets =
 | 
			
		||||
        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
			
		||||
    final newAssetsForAlbum =
 | 
			
		||||
        ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
 | 
			
		||||
    final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
 | 
			
		||||
 | 
			
		||||
    List<Widget> _imageGridGroup = [];
 | 
			
		||||
    List<Widget> imageGridGroup = [];
 | 
			
		||||
 | 
			
		||||
    String _buildAssetCountText() {
 | 
			
		||||
      if (isAlbumExist) {
 | 
			
		||||
@@ -31,19 +33,20 @@ class AssetSelectionPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
    Widget _buildBody() {
 | 
			
		||||
      assetGroupMonthYear.forEach((monthYear, assetGroup) {
 | 
			
		||||
        _imageGridGroup.add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
 | 
			
		||||
        _imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
 | 
			
		||||
        imageGridGroup
 | 
			
		||||
            .add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
 | 
			
		||||
        imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return Stack(
 | 
			
		||||
        children: [
 | 
			
		||||
          DraggableScrollbar.semicircle(
 | 
			
		||||
            backgroundColor: Theme.of(context).primaryColor,
 | 
			
		||||
            controller: _scrollController,
 | 
			
		||||
            controller: scrollController,
 | 
			
		||||
            heightScrollThumb: 48.0,
 | 
			
		||||
            child: CustomScrollView(
 | 
			
		||||
              controller: _scrollController,
 | 
			
		||||
              slivers: [..._imageGridGroup],
 | 
			
		||||
              controller: scrollController,
 | 
			
		||||
              slivers: [...imageGridGroup],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
@@ -71,7 +74,8 @@ class AssetSelectionPage extends HookConsumerWidget {
 | 
			
		||||
              ),
 | 
			
		||||
        centerTitle: false,
 | 
			
		||||
        actions: [
 | 
			
		||||
          (!isAlbumExist && selectedAssets.isNotEmpty) || (isAlbumExist && newAssetsForAlbum.isNotEmpty)
 | 
			
		||||
          (!isAlbumExist && selectedAssets.isNotEmpty) ||
 | 
			
		||||
                  (isAlbumExist && newAssetsForAlbum.isNotEmpty)
 | 
			
		||||
              ? TextButton(
 | 
			
		||||
                  onPressed: () {
 | 
			
		||||
                    var payload = AssetSelectionPageResult(
 | 
			
		||||
 
 | 
			
		||||
@@ -15,11 +15,13 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    final albumTitleController = useTextEditingController.fromValue(TextEditingValue.empty);
 | 
			
		||||
    final albumTitleController =
 | 
			
		||||
        useTextEditingController.fromValue(TextEditingValue.empty);
 | 
			
		||||
    final albumTitleTextFieldFocusNode = useFocusNode();
 | 
			
		||||
    final isAlbumTitleTextFieldFocus = useState(false);
 | 
			
		||||
    final isAlbumTitleEmpty = useState(true);
 | 
			
		||||
    final selectedAssets = ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
			
		||||
    final selectedAssets =
 | 
			
		||||
        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
 | 
			
		||||
 | 
			
		||||
    _showSelectUserPage() {
 | 
			
		||||
      AutoRouter.of(context).push(const SelectUserForSharingRoute());
 | 
			
		||||
@@ -38,8 +40,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
    _onSelectPhotosButtonPressed() async {
 | 
			
		||||
      ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
 | 
			
		||||
 | 
			
		||||
      AssetSelectionPageResult? selectedAsset =
 | 
			
		||||
          await AutoRouter.of(context).push<AssetSelectionPageResult?>(const AssetSelectionRoute());
 | 
			
		||||
      AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
 | 
			
		||||
          .push<AssetSelectionPageResult?>(const AssetSelectionRoute());
 | 
			
		||||
 | 
			
		||||
      if (selectedAsset == null) {
 | 
			
		||||
        ref.watch(assetSelectionProvider.notifier).removeAll();
 | 
			
		||||
@@ -84,16 +86,22 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
            child: OutlinedButton.icon(
 | 
			
		||||
              style: OutlinedButton.styleFrom(
 | 
			
		||||
                  alignment: Alignment.centerLeft,
 | 
			
		||||
                  padding: const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
 | 
			
		||||
                  side: const BorderSide(color: Color.fromARGB(255, 206, 206, 206)),
 | 
			
		||||
                  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))),
 | 
			
		||||
                  padding:
 | 
			
		||||
                      const EdgeInsets.symmetric(vertical: 22, horizontal: 16),
 | 
			
		||||
                  side: const BorderSide(
 | 
			
		||||
                      color: Color.fromARGB(255, 206, 206, 206)),
 | 
			
		||||
                  shape: RoundedRectangleBorder(
 | 
			
		||||
                      borderRadius: BorderRadius.circular(5))),
 | 
			
		||||
              onPressed: _onSelectPhotosButtonPressed,
 | 
			
		||||
              icon: const Icon(Icons.add_rounded),
 | 
			
		||||
              label: Padding(
 | 
			
		||||
                padding: const EdgeInsets.only(left: 8.0),
 | 
			
		||||
                child: Text(
 | 
			
		||||
                  'Select Photos',
 | 
			
		||||
                  style: TextStyle(fontSize: 16, color: Colors.grey[700], fontWeight: FontWeight.bold),
 | 
			
		||||
                  style: TextStyle(
 | 
			
		||||
                      fontSize: 16,
 | 
			
		||||
                      color: Colors.grey[700],
 | 
			
		||||
                      fontWeight: FontWeight.bold),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -141,7 +149,8 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
              (BuildContext context, int index) {
 | 
			
		||||
                return GestureDetector(
 | 
			
		||||
                  onTap: _onBackgroundTapped,
 | 
			
		||||
                  child: SharedAlbumThumbnailImage(asset: selectedAssets.toList()[index]),
 | 
			
		||||
                  child: SharedAlbumThumbnailImage(
 | 
			
		||||
                      asset: selectedAssets.toList()[index]),
 | 
			
		||||
                );
 | 
			
		||||
              },
 | 
			
		||||
              childCount: selectedAssets.length,
 | 
			
		||||
@@ -169,7 +178,9 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
          ),
 | 
			
		||||
          actions: [
 | 
			
		||||
            TextButton(
 | 
			
		||||
              onPressed: albumTitleController.text.isNotEmpty ? _showSelectUserPage : null,
 | 
			
		||||
              onPressed: albumTitleController.text.isNotEmpty
 | 
			
		||||
                  ? _showSelectUserPage
 | 
			
		||||
                  : null,
 | 
			
		||||
              child: const Text(
 | 
			
		||||
                'Share',
 | 
			
		||||
                style: TextStyle(
 | 
			
		||||
@@ -189,13 +200,13 @@ class CreateSharedAlbumPage extends HookConsumerWidget {
 | 
			
		||||
                pinned: true,
 | 
			
		||||
                floating: false,
 | 
			
		||||
                bottom: PreferredSize(
 | 
			
		||||
                  preferredSize: const Size.fromHeight(66.0),
 | 
			
		||||
                  child: Column(
 | 
			
		||||
                    children: [
 | 
			
		||||
                      _buildTitleInputField(),
 | 
			
		||||
                      _buildControlButton(),
 | 
			
		||||
                    ],
 | 
			
		||||
                  ),
 | 
			
		||||
                  preferredSize: const Size.fromHeight(66.0),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              _buildTitle(),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:ffi';
 | 
			
		||||
 | 
			
		||||
class DeviceInfoRemote {
 | 
			
		||||
  final int id;
 | 
			
		||||
@@ -66,7 +65,8 @@ class DeviceInfoRemote {
 | 
			
		||||
 | 
			
		||||
  String toJson() => json.encode(toMap());
 | 
			
		||||
 | 
			
		||||
  factory DeviceInfoRemote.fromJson(String source) => DeviceInfoRemote.fromMap(json.decode(source));
 | 
			
		||||
  factory DeviceInfoRemote.fromJson(String source) =>
 | 
			
		||||
      DeviceInfoRemote.fromMap(json.decode(source));
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() {
 | 
			
		||||
 
 | 
			
		||||
@@ -46,37 +46,45 @@ class AssetNotifier extends StateNotifier<List<ImmichAsset>> {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
 | 
			
		||||
    // final List<String> result = await PhotoManager.editor.deleteWithIds(deleteIdList);
 | 
			
		||||
    await PhotoManager.editor.deleteWithIds(deleteIdList);
 | 
			
		||||
 | 
			
		||||
    // Delete asset on server
 | 
			
		||||
    List<DeleteAssetResponse>? deleteAssetResult = await _assetService.deleteAssets(deleteAssets);
 | 
			
		||||
    List<DeleteAssetResponse>? deleteAssetResult =
 | 
			
		||||
        await _assetService.deleteAssets(deleteAssets);
 | 
			
		||||
    if (deleteAssetResult == null) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (var asset in deleteAssetResult) {
 | 
			
		||||
      if (asset.status == 'success') {
 | 
			
		||||
        state = state.where((immichAsset) => immichAsset.id != asset.id).toList();
 | 
			
		||||
        state =
 | 
			
		||||
            state.where((immichAsset) => immichAsset.id != asset.id).toList();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
 | 
			
		||||
final assetProvider =
 | 
			
		||||
    StateNotifierProvider<AssetNotifier, List<ImmichAsset>>((ref) {
 | 
			
		||||
  return AssetNotifier(ref);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
 | 
			
		||||
  var assets = ref.watch(assetProvider);
 | 
			
		||||
 | 
			
		||||
  assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
 | 
			
		||||
  return assets.groupListsBy((element) => DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
 | 
			
		||||
  assets.sortByCompare<DateTime>(
 | 
			
		||||
      (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
 | 
			
		||||
  return assets.groupListsBy((element) =>
 | 
			
		||||
      DateFormat('y-MM-dd').format(DateTime.parse(element.createdAt)));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final assetGroupByMonthYearProvider = StateProvider((ref) {
 | 
			
		||||
  var assets = ref.watch(assetProvider);
 | 
			
		||||
 | 
			
		||||
  assets.sortByCompare<DateTime>((e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
 | 
			
		||||
  assets.sortByCompare<DateTime>(
 | 
			
		||||
      (e) => DateTime.parse(e.createdAt), (a, b) => b.compareTo(a));
 | 
			
		||||
 | 
			
		||||
  return assets.groupListsBy((element) => DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)));
 | 
			
		||||
  return assets.groupListsBy((element) =>
 | 
			
		||||
      DateFormat('MMMM, y').format(DateTime.parse(element.createdAt)));
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,8 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (latestTagVersion.isNotEmpty && localReleaseVersion != latestTagVersion) {
 | 
			
		||||
        if (latestTagVersion.isNotEmpty &&
 | 
			
		||||
            localReleaseVersion != latestTagVersion) {
 | 
			
		||||
          VersionAnnouncementOverlayController.appLoader.show();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -54,4 +55,5 @@ class ReleaseInfoNotifier extends StateNotifier<String> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>((ref) => ReleaseInfoNotifier());
 | 
			
		||||
final releaseInfoProvider = StateNotifierProvider<ReleaseInfoNotifier, String>(
 | 
			
		||||
    (ref) => ReleaseInfoNotifier());
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,11 @@ import 'dart:convert';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hive/hive.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
			
		||||
import 'package:socket_io_client/socket_io_client.dart';
 | 
			
		||||
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/immich_asset.model.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
			
		||||
import 'package:socket_io_client/socket_io_client.dart';
 | 
			
		||||
 | 
			
		||||
class WebscoketState {
 | 
			
		||||
  final Socket? socket;
 | 
			
		||||
@@ -30,13 +29,16 @@ class WebscoketState {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  String toString() => 'WebscoketState(socket: $socket, isConnected: $isConnected)';
 | 
			
		||||
  String toString() =>
 | 
			
		||||
      'WebscoketState(socket: $socket, isConnected: $isConnected)';
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  bool operator ==(Object other) {
 | 
			
		||||
    if (identical(this, other)) return true;
 | 
			
		||||
 | 
			
		||||
    return other is WebscoketState && other.socket == socket && other.isConnected == isConnected;
 | 
			
		||||
    return other is WebscoketState &&
 | 
			
		||||
        other.socket == socket &&
 | 
			
		||||
        other.isConnected == isConnected;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
@@ -44,7 +46,8 @@ class WebscoketState {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class WebsocketNotifier extends StateNotifier<WebscoketState> {
 | 
			
		||||
  WebsocketNotifier(this.ref) : super(WebscoketState(socket: null, isConnected: false)) {
 | 
			
		||||
  WebsocketNotifier(this.ref)
 | 
			
		||||
      : super(WebscoketState(socket: null, isConnected: false)) {
 | 
			
		||||
    debugPrint("Init websocket instance");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -59,6 +62,7 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
 | 
			
		||||
      try {
 | 
			
		||||
        debugPrint("[WEBSOCKET] Attempting to connect to ws");
 | 
			
		||||
        // Configure socket transports must be sepecified
 | 
			
		||||
 | 
			
		||||
        Socket socket = io(
 | 
			
		||||
          endpoint,
 | 
			
		||||
          OptionBuilder()
 | 
			
		||||
@@ -122,6 +126,7 @@ class WebsocketNotifier extends StateNotifier<WebscoketState> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
 | 
			
		||||
final websocketProvider =
 | 
			
		||||
    StateNotifierProvider<WebsocketNotifier, WebscoketState>((ref) {
 | 
			
		||||
  return WebsocketNotifier(ref);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/server_version.model.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/network.service.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/models/server_info.model.dart';
 | 
			
		||||
 | 
			
		||||
class ServerInfoService {
 | 
			
		||||
  final NetworkService _networkService = NetworkService();
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,9 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
			
		||||
    void goToReleaseNote() async {
 | 
			
		||||
      final Uri _url = Uri.parse('https://github.com/alextran1502/immich/releases/latest');
 | 
			
		||||
      await launchUrl(_url);
 | 
			
		||||
      final Uri url =
 | 
			
		||||
          Uri.parse('https://github.com/alextran1502/immich/releases/latest');
 | 
			
		||||
      await launchUrl(url);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    void onAcknowledgeTapped() {
 | 
			
		||||
@@ -21,7 +22,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ValueListenableBuilder<bool>(
 | 
			
		||||
      valueListenable: VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier,
 | 
			
		||||
      valueListenable:
 | 
			
		||||
          VersionAnnouncementOverlayController.appLoader.loaderShowingNotifier,
 | 
			
		||||
      builder: (context, shouldShow, child) {
 | 
			
		||||
        if (shouldShow) {
 | 
			
		||||
          return Scaffold(
 | 
			
		||||
@@ -51,10 +53,14 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                              child: RichText(
 | 
			
		||||
                                text: TextSpan(
 | 
			
		||||
                                  style: const TextStyle(
 | 
			
		||||
                                      fontSize: 14, fontFamily: 'WorkSans', color: Colors.black87, height: 1.2),
 | 
			
		||||
                                      fontSize: 14,
 | 
			
		||||
                                      fontFamily: 'WorkSans',
 | 
			
		||||
                                      color: Colors.black87,
 | 
			
		||||
                                      height: 1.2),
 | 
			
		||||
                                  children: <TextSpan>[
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text: 'Hi friend, there is a new release of',
 | 
			
		||||
                                      text:
 | 
			
		||||
                                          'Hi friend, there is a new release of',
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text: ' Immich ',
 | 
			
		||||
@@ -65,14 +71,16 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                                      ),
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text: "please take your time to visit the ",
 | 
			
		||||
                                      text:
 | 
			
		||||
                                          "please take your time to visit the ",
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    TextSpan(
 | 
			
		||||
                                      text: "release note",
 | 
			
		||||
                                      style: const TextStyle(
 | 
			
		||||
                                        decoration: TextDecoration.underline,
 | 
			
		||||
                                      ),
 | 
			
		||||
                                      recognizer: TapGestureRecognizer()..onTap = goToReleaseNote,
 | 
			
		||||
                                      recognizer: TapGestureRecognizer()
 | 
			
		||||
                                        ..onTap = goToReleaseNote,
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    const TextSpan(
 | 
			
		||||
                                      text:
 | 
			
		||||
@@ -91,7 +99,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
                                    primary: Colors.indigo,
 | 
			
		||||
                                    onPrimary: Colors.grey[50],
 | 
			
		||||
                                    elevation: 2,
 | 
			
		||||
                                    padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25),
 | 
			
		||||
                                    padding: const EdgeInsets.symmetric(
 | 
			
		||||
                                        vertical: 10, horizontal: 25),
 | 
			
		||||
                                  ),
 | 
			
		||||
                                  onPressed: onAcknowledgeTapped,
 | 
			
		||||
                                  child: const Text(
 | 
			
		||||
@@ -119,7 +128,8 @@ class VersionAnnouncementOverlay extends HookConsumerWidget {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class VersionAnnouncementOverlayController {
 | 
			
		||||
  static final VersionAnnouncementOverlayController appLoader = VersionAnnouncementOverlayController();
 | 
			
		||||
  static final VersionAnnouncementOverlayController appLoader =
 | 
			
		||||
      VersionAnnouncementOverlayController();
 | 
			
		||||
  ValueNotifier<bool> loaderShowingNotifier = ValueNotifier(false);
 | 
			
		||||
  ValueNotifier<String> loaderTextNotifier = ValueNotifier('error message');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import 'package:dio/dio.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hive_flutter/hive_flutter.dart';
 | 
			
		||||
import 'package:immich_mobile/constants/hive_box.dart';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -198,7 +198,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.1.0"
 | 
			
		||||
  collection:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: collection
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
@@ -233,7 +233,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.17.1"
 | 
			
		||||
  cupertino_icons:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
      name: cupertino_icons
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
@@ -334,7 +334,7 @@ packages:
 | 
			
		||||
      name: flutter_lints
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.0.4"
 | 
			
		||||
    version: "2.0.1"
 | 
			
		||||
  flutter_map:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -465,12 +465,12 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "3.2.0"
 | 
			
		||||
  http_parser:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: http_parser
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.0.0"
 | 
			
		||||
    version: "4.0.1"
 | 
			
		||||
  image:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -542,7 +542,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "4.5.0"
 | 
			
		||||
  latlong2:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: latlong2
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
@@ -554,7 +554,7 @@ packages:
 | 
			
		||||
      name: lints
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.0.1"
 | 
			
		||||
    version: "2.0.0"
 | 
			
		||||
  lists:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -668,19 +668,19 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.0.5"
 | 
			
		||||
  path:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: path
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.8.1"
 | 
			
		||||
  path_provider:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: path_provider
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.10"
 | 
			
		||||
    version: "2.0.11"
 | 
			
		||||
  path_provider_android:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
@@ -861,13 +861,6 @@ packages:
 | 
			
		||||
    description: flutter
 | 
			
		||||
    source: sdk
 | 
			
		||||
    version: "0.0.99"
 | 
			
		||||
  sliver_tools:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: sliver_tools
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.2.6"
 | 
			
		||||
  socket_io_client:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
@@ -1141,13 +1134,6 @@ packages:
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "2.0.10"
 | 
			
		||||
  visibility_detector:
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: visibility_detector
 | 
			
		||||
      url: "https://pub.dartlang.org"
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "0.2.2"
 | 
			
		||||
  wakelock:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    description:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ environment:
 | 
			
		||||
dependencies:
 | 
			
		||||
  flutter:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
  cupertino_icons: ^1.0.2
 | 
			
		||||
 | 
			
		||||
  photo_manager: ^2.0.6
 | 
			
		||||
  flutter_hooks: ^0.18.0
 | 
			
		||||
  hooks_riverpod: ^2.0.0-dev.0
 | 
			
		||||
@@ -23,12 +23,10 @@ dependencies:
 | 
			
		||||
  auto_route: ^4.0.1
 | 
			
		||||
  exif: ^3.1.1
 | 
			
		||||
  transparent_image: ^2.0.0
 | 
			
		||||
  visibility_detector: ^0.2.2
 | 
			
		||||
  flutter_launcher_icons: "^0.9.2"
 | 
			
		||||
  fluttertoast: ^8.0.8
 | 
			
		||||
  video_player: ^2.2.18
 | 
			
		||||
  chewie: ^1.2.2
 | 
			
		||||
  sliver_tools: ^0.2.5
 | 
			
		||||
  badges: ^2.0.2
 | 
			
		||||
  photo_view: ^0.14.0
 | 
			
		||||
  socket_io_client: ^2.0.0-beta.4-nullsafety.0
 | 
			
		||||
@@ -43,10 +41,17 @@ dependencies:
 | 
			
		||||
  http: 0.13.4
 | 
			
		||||
  cancellation_token_http: ^1.1.0
 | 
			
		||||
 | 
			
		||||
  path: ^1.8.1
 | 
			
		||||
  path_provider: ^2.0.11
 | 
			
		||||
  latlong2: ^0.8.1
 | 
			
		||||
  collection: ^1.16.0
 | 
			
		||||
  http_parser: ^4.0.1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
    sdk: flutter
 | 
			
		||||
  flutter_lints: ^1.0.0
 | 
			
		||||
  flutter_lints: ^2.0.1
 | 
			
		||||
  hive_generator: ^1.1.2
 | 
			
		||||
  build_runner: ^2.1.7
 | 
			
		||||
  auto_route_generator: ^4.0.0
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user