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 0b2605b949..489d011c2c 100644 --- a/mobile/lib/shared/models/user.g.dart +++ b/mobile/lib/shared/models/user.g.dart @@ -28,48 +28,63 @@ const UserSchema = CollectionSchema( name: r'email', type: IsarType.string, ), - r'id': PropertySchema( + r'hasQuota': PropertySchema( id: 2, + name: r'hasQuota', + type: IsarType.bool, + ), + r'id': PropertySchema( + id: 3, name: r'id', type: IsarType.string, ), r'inTimeline': PropertySchema( - id: 3, + id: 4, name: r'inTimeline', type: IsarType.bool, ), r'isAdmin': PropertySchema( - id: 4, + id: 5, name: r'isAdmin', type: IsarType.bool, ), r'isPartnerSharedBy': PropertySchema( - id: 5, + id: 6, name: r'isPartnerSharedBy', type: IsarType.bool, ), r'isPartnerSharedWith': PropertySchema( - id: 6, + id: 7, name: r'isPartnerSharedWith', type: IsarType.bool, ), r'memoryEnabled': PropertySchema( - id: 7, + id: 8, name: r'memoryEnabled', type: IsarType.bool, ), r'name': PropertySchema( - id: 8, + id: 9, name: r'name', type: IsarType.string, ), r'profileImagePath': PropertySchema( - id: 9, + id: 10, name: r'profileImagePath', type: IsarType.string, ), + r'quotaSizeInBytes': PropertySchema( + id: 11, + name: r'quotaSizeInBytes', + type: IsarType.long, + ), + r'quotaUsageInBytes': PropertySchema( + id: 12, + name: r'quotaUsageInBytes', + type: IsarType.long, + ), r'updatedAt': PropertySchema( - id: 10, + id: 13, name: r'updatedAt', type: IsarType.dateTime, ) @@ -138,15 +153,18 @@ void _userSerialize( ) { writer.writeByte(offsets[0], object.avatarColor.index); writer.writeString(offsets[1], object.email); - writer.writeString(offsets[2], object.id); - writer.writeBool(offsets[3], object.inTimeline); - writer.writeBool(offsets[4], object.isAdmin); - writer.writeBool(offsets[5], object.isPartnerSharedBy); - writer.writeBool(offsets[6], object.isPartnerSharedWith); - writer.writeBool(offsets[7], object.memoryEnabled); - writer.writeString(offsets[8], object.name); - writer.writeString(offsets[9], object.profileImagePath); - writer.writeDateTime(offsets[10], object.updatedAt); + writer.writeBool(offsets[2], object.hasQuota); + writer.writeString(offsets[3], object.id); + writer.writeBool(offsets[4], object.inTimeline); + writer.writeBool(offsets[5], object.isAdmin); + writer.writeBool(offsets[6], object.isPartnerSharedBy); + writer.writeBool(offsets[7], object.isPartnerSharedWith); + writer.writeBool(offsets[8], object.memoryEnabled); + writer.writeString(offsets[9], object.name); + writer.writeString(offsets[10], object.profileImagePath); + writer.writeLong(offsets[11], object.quotaSizeInBytes); + writer.writeLong(offsets[12], object.quotaUsageInBytes); + writer.writeDateTime(offsets[13], object.updatedAt); } User _userDeserialize( @@ -160,15 +178,17 @@ User _userDeserialize( _UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ?? AvatarColorEnum.primary, email: reader.readString(offsets[1]), - id: reader.readString(offsets[2]), - inTimeline: reader.readBoolOrNull(offsets[3]) ?? false, - isAdmin: reader.readBool(offsets[4]), - isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false, - isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true, - name: reader.readString(offsets[8]), - profileImagePath: reader.readStringOrNull(offsets[9]) ?? '', - updatedAt: reader.readDateTime(offsets[10]), + id: reader.readString(offsets[3]), + inTimeline: reader.readBoolOrNull(offsets[4]) ?? false, + isAdmin: reader.readBool(offsets[5]), + isPartnerSharedBy: reader.readBoolOrNull(offsets[6]) ?? false, + isPartnerSharedWith: reader.readBoolOrNull(offsets[7]) ?? false, + memoryEnabled: reader.readBoolOrNull(offsets[8]) ?? true, + name: reader.readString(offsets[9]), + profileImagePath: reader.readStringOrNull(offsets[10]) ?? '', + quotaSizeInBytes: reader.readLongOrNull(offsets[11]) ?? 0, + quotaUsageInBytes: reader.readLongOrNull(offsets[12]) ?? 0, + updatedAt: reader.readDateTime(offsets[13]), ); return object; } @@ -186,22 +206,28 @@ P _userDeserializeProp

( case 1: return (reader.readString(offset)) as P; case 2: - return (reader.readString(offset)) as P; - case 3: - return (reader.readBoolOrNull(offset) ?? false) as P; - case 4: return (reader.readBool(offset)) as P; - case 5: + case 3: + return (reader.readString(offset)) as P; + case 4: return (reader.readBoolOrNull(offset) ?? false) as P; + case 5: + return (reader.readBool(offset)) as P; case 6: return (reader.readBoolOrNull(offset) ?? false) as P; case 7: - return (reader.readBoolOrNull(offset) ?? true) as P; + return (reader.readBoolOrNull(offset) ?? false) as P; case 8: - return (reader.readString(offset)) as P; + return (reader.readBoolOrNull(offset) ?? true) as P; case 9: - return (reader.readStringOrNull(offset) ?? '') as P; + return (reader.readString(offset)) as P; case 10: + return (reader.readStringOrNull(offset) ?? '') as P; + case 11: + return (reader.readLongOrNull(offset) ?? 0) as P; + case 12: + return (reader.readLongOrNull(offset) ?? 0) as P; + case 13: return (reader.readDateTime(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -601,6 +627,15 @@ extension UserQueryFilter on QueryBuilder { }); } + QueryBuilder hasQuotaEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'hasQuota', + value: value, + )); + }); + } + QueryBuilder idEqualTo( String value, { bool caseSensitive = true, @@ -1088,6 +1123,112 @@ extension UserQueryFilter on QueryBuilder { }); } + QueryBuilder quotaSizeInBytesEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'quotaSizeInBytes', + value: value, + )); + }); + } + + QueryBuilder quotaSizeInBytesGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'quotaSizeInBytes', + value: value, + )); + }); + } + + QueryBuilder quotaSizeInBytesLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'quotaSizeInBytes', + value: value, + )); + }); + } + + QueryBuilder quotaSizeInBytesBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'quotaSizeInBytes', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder quotaUsageInBytesEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'quotaUsageInBytes', + value: value, + )); + }); + } + + QueryBuilder quotaUsageInBytesGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'quotaUsageInBytes', + value: value, + )); + }); + } + + QueryBuilder quotaUsageInBytesLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'quotaUsageInBytes', + value: value, + )); + }); + } + + QueryBuilder quotaUsageInBytesBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'quotaUsageInBytes', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder updatedAtEqualTo( DateTime value) { return QueryBuilder.apply(this, (query) { @@ -1282,6 +1423,18 @@ extension UserQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByHasQuota() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'hasQuota', Sort.asc); + }); + } + + QueryBuilder sortByHasQuotaDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'hasQuota', Sort.desc); + }); + } + QueryBuilder sortById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1378,6 +1531,30 @@ extension UserQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByQuotaSizeInBytes() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaSizeInBytes', Sort.asc); + }); + } + + QueryBuilder sortByQuotaSizeInBytesDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaSizeInBytes', Sort.desc); + }); + } + + QueryBuilder sortByQuotaUsageInBytes() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaUsageInBytes', Sort.asc); + }); + } + + QueryBuilder sortByQuotaUsageInBytesDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaUsageInBytes', Sort.desc); + }); + } + QueryBuilder sortByUpdatedAt() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'updatedAt', Sort.asc); @@ -1416,6 +1593,18 @@ extension UserQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByHasQuota() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'hasQuota', Sort.asc); + }); + } + + QueryBuilder thenByHasQuotaDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'hasQuota', Sort.desc); + }); + } + QueryBuilder thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1524,6 +1713,30 @@ extension UserQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByQuotaSizeInBytes() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaSizeInBytes', Sort.asc); + }); + } + + QueryBuilder thenByQuotaSizeInBytesDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaSizeInBytes', Sort.desc); + }); + } + + QueryBuilder thenByQuotaUsageInBytes() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaUsageInBytes', Sort.asc); + }); + } + + QueryBuilder thenByQuotaUsageInBytesDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'quotaUsageInBytes', Sort.desc); + }); + } + QueryBuilder thenByUpdatedAt() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'updatedAt', Sort.asc); @@ -1551,6 +1764,12 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByHasQuota() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'hasQuota'); + }); + } + QueryBuilder distinctById( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1603,6 +1822,18 @@ extension UserQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByQuotaSizeInBytes() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'quotaSizeInBytes'); + }); + } + + QueryBuilder distinctByQuotaUsageInBytes() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'quotaUsageInBytes'); + }); + } + QueryBuilder distinctByUpdatedAt() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'updatedAt'); @@ -1629,6 +1860,12 @@ extension UserQueryProperty on QueryBuilder { }); } + QueryBuilder hasQuotaProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'hasQuota'); + }); + } + QueryBuilder idProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'id'); @@ -1677,6 +1914,18 @@ extension UserQueryProperty on QueryBuilder { }); } + QueryBuilder quotaSizeInBytesProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'quotaSizeInBytes'); + }); + } + + QueryBuilder quotaUsageInBytesProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'quotaUsageInBytes'); + }); + } + QueryBuilder updatedAtProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'updatedAt'); diff --git a/mobile/lib/shared/providers/user.provider.dart b/mobile/lib/shared/providers/user.provider.dart index 61c77b24b7..fbbc399957 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 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 7a3a7bf566..5dc4b7aaf7 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -70,7 +70,7 @@ class PartnerResponseDto { int? quotaSizeInBytes; - int quotaUsageInBytes; + int? quotaUsageInBytes; bool shouldChangePassword; @@ -114,7 +114,7 @@ class PartnerResponseDto { (oauthId.hashCode) + (profileImagePath.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + - (quotaUsageInBytes.hashCode) + + (quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) + (shouldChangePassword.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode) + (updatedAt.hashCode); @@ -157,7 +157,11 @@ class PartnerResponseDto { } else { // json[r'quotaSizeInBytes'] = null; } + if (this.quotaUsageInBytes != null) { json[r'quotaUsageInBytes'] = this.quotaUsageInBytes; + } else { + // json[r'quotaUsageInBytes'] = null; + } json[r'shouldChangePassword'] = this.shouldChangePassword; if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; @@ -189,7 +193,7 @@ class PartnerResponseDto { oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), - quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, + quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, storageLabel: mapValueOfType(json, r'storageLabel'), updatedAt: mapDateTime(json, r'updatedAt', '')!, diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 0f2e2eaf2d..34e98afbf1 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -61,7 +61,7 @@ class UserResponseDto { int? quotaSizeInBytes; - int quotaUsageInBytes; + int? quotaUsageInBytes; bool shouldChangePassword; @@ -103,7 +103,7 @@ class UserResponseDto { (oauthId.hashCode) + (profileImagePath.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + - (quotaUsageInBytes.hashCode) + + (quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) + (shouldChangePassword.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode) + (updatedAt.hashCode); @@ -141,7 +141,11 @@ class UserResponseDto { } else { // json[r'quotaSizeInBytes'] = null; } + if (this.quotaUsageInBytes != null) { json[r'quotaUsageInBytes'] = this.quotaUsageInBytes; + } else { + // json[r'quotaUsageInBytes'] = null; + } json[r'shouldChangePassword'] = this.shouldChangePassword; if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; @@ -172,7 +176,7 @@ class UserResponseDto { oauthId: mapValueOfType(json, r'oauthId')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), - quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, + quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes'), shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, storageLabel: mapValueOfType(json, r'storageLabel'), updatedAt: mapDateTime(json, r'updatedAt', '')!, 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 => {