diff --git a/mobile/.fvm/fvm_config.json b/mobile/.fvm/fvm_config.json deleted file mode 100644 index 04c1b862c..000000000 --- a/mobile/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "3.13.0", - "flavors": {} -} diff --git a/mobile/.fvmrc b/mobile/.fvmrc new file mode 100644 index 000000000..54ef3132c --- /dev/null +++ b/mobile/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.13.6" +} \ No newline at end of file diff --git a/mobile/.gitignore b/mobile/.gitignore index f643592c0..fc807ea68 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -31,7 +31,6 @@ .pub-cache/ .pub/ /build/ -.fvm/flutter_sdk # Web related lib/generated_plugin_registrant.dart @@ -53,4 +52,7 @@ ios/fastlane/report.xml # Isar default.isar default.isar.lock -libisar.so \ No newline at end of file +libisar.so + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index 89183ccc3..d7e3ad9fd 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,10 +1,8 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - // Remove .fvm files from search + "dart.flutterSdkPath": ".fvm\\versions\\3.13.6", "search.exclude": { "**/.fvm": true }, - // Remove from file watching "files.watcherExclude": { "**/.fvm": true } diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 45f2dc1c5..24a209cec 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -179,4 +179,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index b46dee8f4..293867fb3 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -81,6 +81,7 @@ Future initApp() async { PlatformDispatcher.instance.onError = (error, stack) { log.severe('PlatformDispatcher - Catch all error: $error', error, stack); + debugPrint("PlatformDispatcher - Catch all error: $error $stack"); return true; }; diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index 05c60ea28..3fe4761d6 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -363,6 +363,7 @@ class BackupService { } else { var data = await response.stream.bytesToString(); var error = jsonDecode(data); + var errorMessage = error['message'] ?? error['error']; debugPrint( "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", @@ -375,9 +376,14 @@ class BackupService { fileCreatedAt: entity.createDateTime, fileName: originalFileName, fileType: _getAssetType(entity.type), - errorMessage: error['error'], + errorMessage: errorMessage, ), ); + + if (errorMessage == "Quota has been exceeded!") { + anyErrors = true; + break; + } continue; } } diff --git a/mobile/lib/modules/backup/views/failed_backup_status_page.dart b/mobile/lib/modules/backup/views/failed_backup_status_page.dart index 7af47d6f9..5679a4166 100644 --- a/mobile/lib/modules/backup/views/failed_backup_status_page.dart +++ b/mobile/lib/modules/backup/views/failed_backup_status_page.dart @@ -57,9 +57,9 @@ class FailedBackupStatusPage extends HookConsumerWidget { ConstrainedBox( constraints: const BoxConstraints( minWidth: 100, - minHeight: 150, + minHeight: 100, maxWidth: 100, - maxHeight: 200, + maxHeight: 150, ), child: ClipRRect( borderRadius: const BorderRadius.only( @@ -95,9 +95,10 @@ class FailedBackupStatusPage extends HookConsumerWidget { ).toLocal(), ), style: TextStyle( - fontSize: 12, fontWeight: FontWeight.w600, - color: Colors.grey[700], + color: context.isDarkTheme + ? Colors.white70 + : Colors.grey[800], ), ), Icon( @@ -115,7 +116,6 @@ class FailedBackupStatusPage extends HookConsumerWidget { overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.bold, - fontSize: 12, color: context.primaryColor, ), ), @@ -123,9 +123,10 @@ class FailedBackupStatusPage extends HookConsumerWidget { Text( errorAsset.errorMessage, style: TextStyle( - fontSize: 12, fontWeight: FontWeight.w500, - color: Colors.grey[800], + color: context.isDarkTheme + ? Colors.white70 + : Colors.grey[800], ), ), ], diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index aec63e7be..614250bdf 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -21,6 +21,8 @@ class User { this.avatarColor = AvatarColorEnum.primary, this.memoryEnabled = true, this.inTimeline = false, + this.quotaUsageInBytes = 0, + this.quotaSizeInBytes = 0, }); Id get isarId => fastHash(id); @@ -36,7 +38,9 @@ class User { isAdmin = dto.isAdmin, memoryEnabled = dto.memoriesEnabled ?? false, avatarColor = dto.avatarColor.toAvatarColor(), - inTimeline = false; + inTimeline = false, + quotaUsageInBytes = dto.quotaUsageInBytes ?? 0, + quotaSizeInBytes = dto.quotaSizeInBytes ?? 0; User.fromPartnerDto(PartnerResponseDto dto) : id = dto.id, @@ -49,7 +53,9 @@ class User { isAdmin = dto.isAdmin, memoryEnabled = dto.memoriesEnabled ?? false, avatarColor = dto.avatarColor.toAvatarColor(), - inTimeline = dto.inTimeline ?? false; + inTimeline = dto.inTimeline ?? false, + quotaUsageInBytes = dto.quotaUsageInBytes ?? 0, + quotaSizeInBytes = dto.quotaSizeInBytes ?? 0; /// Base user dto used where the complete user object is not required User.fromSimpleUserDto(UserDto dto) @@ -64,7 +70,9 @@ class User { memoryEnabled = false, isPartnerSharedBy = false, isPartnerSharedWith = false, - updatedAt = DateTime.now(); + updatedAt = DateTime.now(), + quotaUsageInBytes = 0, + quotaSizeInBytes = 0; @Index(unique: true, replace: false, type: IndexType.hash) String id; @@ -79,7 +87,10 @@ class User { AvatarColorEnum avatarColor; bool memoryEnabled; bool inTimeline; + int quotaUsageInBytes; + int quotaSizeInBytes; + bool get hasQuota => quotaSizeInBytes > 0; @Backlink(to: 'owner') final IsarLinks albums = IsarLinks(); @Backlink(to: 'sharedUsers') @@ -98,7 +109,9 @@ class User { profileImagePath == other.profileImagePath && isAdmin == other.isAdmin && memoryEnabled == other.memoryEnabled && - inTimeline == other.inTimeline; + inTimeline == other.inTimeline && + quotaUsageInBytes == other.quotaUsageInBytes && + quotaSizeInBytes == other.quotaSizeInBytes; } @override @@ -114,7 +127,9 @@ class User { avatarColor.hashCode ^ isAdmin.hashCode ^ memoryEnabled.hashCode ^ - inTimeline.hashCode; + inTimeline.hashCode ^ + quotaUsageInBytes.hashCode ^ + quotaSizeInBytes.hashCode; } enum AvatarColorEnum { diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index 0b2605b94..489d011c2 100644 Binary files a/mobile/lib/shared/models/user.g.dart and b/mobile/lib/shared/models/user.g.dart differ diff --git a/mobile/lib/shared/providers/user.provider.dart b/mobile/lib/shared/providers/user.provider.dart index 61c77b24b..fbbc39995 100644 --- a/mobile/lib/shared/providers/user.provider.dart +++ b/mobile/lib/shared/providers/user.provider.dart @@ -3,18 +3,33 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:isar/isar.dart'; class CurrentUserProvider extends StateNotifier { - CurrentUserProvider() : super(null) { + CurrentUserProvider(this._apiService) : super(null) { state = Store.tryGet(StoreKey.currentUser); streamSub = Store.watch(StoreKey.currentUser).listen((user) => state = user); } + final ApiService _apiService; late final StreamSubscription streamSub; + refresh() async { + try { + final user = await _apiService.userApi.getMyUserInfo(); + if (user != null) { + Store.put( + StoreKey.currentUser, + User.fromUserDto(user), + ); + } + } catch (_) {} + } + @override void dispose() { streamSub.cancel(); @@ -24,7 +39,9 @@ class CurrentUserProvider extends StateNotifier { final currentUserProvider = StateNotifierProvider((ref) { - return CurrentUserProvider(); + return CurrentUserProvider( + ref.watch(apiServiceProvider), + ); }); class TimelineUserIdsProvider extends StateNotifier> { diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart index 856d74f16..24ee7e693 100644 --- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart @@ -15,6 +15,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_profile_info.dart'; import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_server_info.dart'; import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:url_launcher/url_launcher.dart'; class ImmichAppBarDialog extends HookConsumerWidget { @@ -31,6 +32,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { useEffect( () { ref.read(backupProvider.notifier).updateServerInfo(); + ref.read(currentUserProvider.notifier).refresh(); return null; }, [user], @@ -132,6 +134,16 @@ class ImmichAppBarDialog extends HookConsumerWidget { } Widget buildStorageInformation() { + var percentage = backupState.serverInfo.diskUsagePercentage / 100; + var usedDiskSpace = backupState.serverInfo.diskUse; + var totalDiskSpace = backupState.serverInfo.diskSize; + + if (user != null && user.hasQuota) { + usedDiskSpace = formatBytes(user.quotaUsageInBytes); + totalDiskSpace = formatBytes(user.quotaSizeInBytes); + percentage = user.quotaUsageInBytes / user.quotaSizeInBytes; + } + return Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3), child: Container( @@ -163,7 +175,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8.0), child: LinearProgressIndicator( minHeight: 5.0, - value: backupState.serverInfo.diskUsagePercentage / 100.0, + value: percentage, backgroundColor: Colors.grey, color: theme.primaryColor, ), @@ -173,8 +185,8 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: const Text('backup_controller_page_storage_format').tr( args: [ - backupState.serverInfo.diskUse, - backupState.serverInfo.diskSize, + usedDiskSpace, + totalDiskSpace, ], ), ), diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 7a3a7bf56..5dc4b7aaf 100644 Binary files a/mobile/openapi/lib/model/partner_response_dto.dart and b/mobile/openapi/lib/model/partner_response_dto.dart differ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 0f2e2eaf2..34e98afbf 100644 Binary files a/mobile/openapi/lib/model/user_response_dto.dart and b/mobile/openapi/lib/model/user_response_dto.dart differ diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 14b9c2e78..bbde4ac5c 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -739,10 +739,10 @@ packages: dependency: transitive description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.4" image_picker: dependency: "direct main" description: diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 112174b49..61f2d26c9 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8204,6 +8204,7 @@ }, "quotaUsageInBytes": { "format": "int64", + "nullable": true, "type": "integer" }, "shouldChangePassword": { @@ -9912,6 +9913,7 @@ }, "quotaUsageInBytes": { "format": "int64", + "nullable": true, "type": "integer" }, "shouldChangePassword": { diff --git a/open-api/typescript-sdk/client/api.ts b/open-api/typescript-sdk/client/api.ts index 7f0587319..f6e1e9532 100644 --- a/open-api/typescript-sdk/client/api.ts +++ b/open-api/typescript-sdk/client/api.ts @@ -2496,7 +2496,7 @@ export interface PartnerResponseDto { * @type {number} * @memberof PartnerResponseDto */ - 'quotaUsageInBytes': number; + 'quotaUsageInBytes': number | null; /** * * @type {boolean} @@ -4770,7 +4770,7 @@ export interface UserResponseDto { * @type {number} * @memberof UserResponseDto */ - 'quotaUsageInBytes': number; + 'quotaUsageInBytes': number | null; /** * * @type {boolean} diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts index 7ef0b98b3..e6dff1655 100644 --- a/server/src/domain/user/response-dto/user-response.dto.ts +++ b/server/src/domain/user/response-dto/user-response.dto.ts @@ -36,7 +36,7 @@ export class UserResponseDto extends UserDto { @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes!: number | null; @ApiProperty({ type: 'integer', format: 'int64' }) - quotaUsageInBytes!: number; + quotaUsageInBytes!: number | null; } export const mapSimpleUser = (entity: UserEntity): UserDto => {