From 78de4f1312fa045fccf6d7361ae916f1d90964a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 16 Jan 2024 20:08:31 -0600 Subject: [PATCH] feat(mobile): quota (#6409) * feat(mobile): quota * openapi * user entity update * Render quota * refresh usage upon opening the app bar * stop backup when quota exceed --- mobile/.fvm/fvm_config.json | 4 --- mobile/.fvmrc | 3 +++ mobile/.gitignore | 6 +++-- mobile/.vscode/settings.json | 4 +-- mobile/ios/Podfile.lock | 2 +- mobile/lib/main.dart | 1 + .../backup/services/backup.service.dart | 8 +++++- .../views/failed_backup_status_page.dart | 15 ++++++----- mobile/lib/shared/models/user.dart | 25 ++++++++++++++---- mobile/lib/shared/models/user.g.dart | Bin 48120 -> 55447 bytes .../lib/shared/providers/user.provider.dart | 21 +++++++++++++-- .../ui/app_bar_dialog/app_bar_dialog.dart | 18 ++++++++++--- .../lib/model/partner_response_dto.dart | Bin 8782 -> 8919 bytes .../openapi/lib/model/user_response_dto.dart | Bin 8022 -> 8159 bytes mobile/pubspec.lock | 8 +++--- open-api/immich-openapi-specs.json | 2 ++ open-api/typescript-sdk/client/api.ts | 4 +-- .../user/response-dto/user-response.dto.ts | 2 +- 18 files changed, 88 insertions(+), 35 deletions(-) delete mode 100644 mobile/.fvm/fvm_config.json create mode 100644 mobile/.fvmrc diff --git a/mobile/.fvm/fvm_config.json b/mobile/.fvm/fvm_config.json deleted file mode 100644 index 04c1b862c9..0000000000 --- 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 0000000000..54ef3132c8 --- /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 f643592c03..fc807ea681 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 89183ccc38..d7e3ad9fd0 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 45f2dc1c52..24a209cec2 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 b46dee8f41..293867fb32 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 05c60ea28d..3fe4761d6a 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 7af47d6f93..5679a41664 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 aec63e7bea..614250bdf6 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 0b2605b949894978dde8a31a66d1d04c114dabfc..489d011c2c90db30cbc5a30097763e82a8381999 100644 GIT binary patch delta 2099 zcma)7O-$2Z7*1j9rU>0SH$;V$A=w5Cu0tb;MxyvP5q|)KBE*VCFmnrZ1SJ|Z@jyh4 z@iBpeM!lG*7=Ic?V&cz5FDB^0ix)K}8WRu33&ex?b)B|bi@L+!^nIWAd7tO)*LG?$ zYxr*Vod*_smmqEHizfs;%`U_ni&INHwgxd~DZ&aXi_ey3TqCi;+Jb!Ag>h>;F3=o8 zS1qWacOuv3gr8>c?Y0%~8tr&t_2MI4hJKPSvhBnt5(jPV*kxxiX&62QR&sV~SP5IJrgfJ@_ zM~S_Ps+4!z{WKbwTvxa)$i?>_5IYhTfA({m;iyVn$Sb3k;7MMXT8i&^i_yx=M-8bg z!)~UAs=^4f3_qD1yk!<;@gYnz<@@T1UVpr^QxX$WE6;1xneGBL#D0HocS7u~NQU%I zov37InvViU2{EwJ5u=F6QD>Nld`JG5f@Q05j%CrtR$_v+VVJEVGGDPFsv4AQ`7u7! z7T{K`;(R`JS(&29Z4iZhM3E8{L0fto`l3<2p=5tZ*REIgkN(-Md{`9_LY=FEc#vFY zDV%f{f^#p#gonn}#< z+MYfk+7$Pvj(K-1f!f9)>CFq9iqS2{Cn?GORe_ zBJD@e&|YOqVNAXmyoLAGwZ_84`lA?Myr#r(6wN(44LA_VCRv?JlPc*^m#RNvfAwij zVvM(tZ)~b%79a^<#Xp@|KW9lCRGF(TrN`~XL=Wo#Rql=g3 z7;Vg8I$}i5U|8Wc@i3F;hzWKkzl55Rh3l2snlb0i9kYIa1$=nz3(Chw*U?6?1gEZ0 z_gzckaeQ>Hi3EaY4Nt|N2Ko8DuUW8<&C`z2<%fIHgp}gj_nRi zWI+<;af*$NV&%EBu(4u8vLZ#!*5?1~um4*)O8ojvI6Sc-Q_jSrjUg;|pg~q~DtBUA zbQbA{lej>YRBV!7OcM+DTLd{u8-57GtkQ)Eh2cZ#!;|QXQ4`&AE|%;*v?MWpZ8r#} znNtcPj}}VhXrV!bX$t0Pu2hLPYT}w&IHn%K+K|jL@JbyhXC+cKBCHex%z-tQ z|6QuhY%H7kh?`!#=uST7DJ$Z8O*<7ho3-hCK`tf=SKRK^Y;qUizGCm(R{{V { - 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 856d74f168..24ee7e693f 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 7a3a7bf56696ea5da95d86a01cbc424331ca8c3c..5dc4b7aaf752c45c74eea674a82e55429f24cc63 100644 GIT binary patch delta 90 zcmV-g0Hy!VMAt>IV+R30vttL!1sEVbJs@s%Y-}JuATS_0AaQkXbYWFf#kP;b_JQOmM*Af_$QyLPJ{}LUucNFghlNuTdvvC^T2QnZXPXGV_ delta 37 vcmV+=0NVf8Mb1R9V+XTj2g(JrPYnhKlg|+elZX^Dv-lMF1py(m@EYd_9BvM{ diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 0f2e2eaf2df8831f7be18a45805e1fcf944ce9fc..34e98afbf1ba6ad3136662cb88aa0bc50c60f468 100644 GIT binary patch delta 87 zcmca+ci(=)WmZP}&6ioTSS1u}Z58rLb8-~y6$})t6bei8OAgcH$N3z&bql*{3JU74q+XS delta 53 zcmca_f6Z>gWmZOo$$~<{n_sYov2Kp%PGOt;R?vELn$QMTZbcm~1t^$Y$StzjL}E2N E0PB?y2><{9 diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 14b9c2e786..bbde4ac5cd 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 112174b492..61f2d26c93 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 7f0587319f..f6e1e95326 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 7ef0b98b3c..e6dff1655c 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 => {