1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

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
This commit is contained in:
Alex 2024-01-16 20:08:31 -06:00 committed by GitHub
parent abce82e235
commit 78de4f1312
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 88 additions and 35 deletions

View file

@ -1,4 +0,0 @@
{
"flutterSdkVersion": "3.13.0",
"flavors": {}
}

3
mobile/.fvmrc Normal file
View file

@ -0,0 +1,3 @@
{
"flutter": "3.13.6"
}

6
mobile/.gitignore vendored
View file

@ -31,7 +31,6 @@
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
.fvm/flutter_sdk
# Web related # Web related
lib/generated_plugin_registrant.dart lib/generated_plugin_registrant.dart
@ -53,4 +52,7 @@ ios/fastlane/report.xml
# Isar # Isar
default.isar default.isar
default.isar.lock default.isar.lock
libisar.so libisar.so
# FVM Version Cache
.fvm/

View file

@ -1,10 +1,8 @@
{ {
"dart.flutterSdkPath": ".fvm/flutter_sdk", "dart.flutterSdkPath": ".fvm\\versions\\3.13.6",
// Remove .fvm files from search
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },
// Remove from file watching
"files.watcherExclude": { "files.watcherExclude": {
"**/.fvm": true "**/.fvm": true
} }

View file

@ -179,4 +179,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.12.1 COCOAPODS: 1.11.3

View file

@ -81,6 +81,7 @@ Future<void> initApp() async {
PlatformDispatcher.instance.onError = (error, stack) { PlatformDispatcher.instance.onError = (error, stack) {
log.severe('PlatformDispatcher - Catch all error: $error', error, stack); log.severe('PlatformDispatcher - Catch all error: $error', error, stack);
debugPrint("PlatformDispatcher - Catch all error: $error $stack");
return true; return true;
}; };

View file

@ -363,6 +363,7 @@ class BackupService {
} else { } else {
var data = await response.stream.bytesToString(); var data = await response.stream.bytesToString();
var error = jsonDecode(data); var error = jsonDecode(data);
var errorMessage = error['message'] ?? error['error'];
debugPrint( debugPrint(
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
@ -375,9 +376,14 @@ class BackupService {
fileCreatedAt: entity.createDateTime, fileCreatedAt: entity.createDateTime,
fileName: originalFileName, fileName: originalFileName,
fileType: _getAssetType(entity.type), fileType: _getAssetType(entity.type),
errorMessage: error['error'], errorMessage: errorMessage,
), ),
); );
if (errorMessage == "Quota has been exceeded!") {
anyErrors = true;
break;
}
continue; continue;
} }
} }

View file

@ -57,9 +57,9 @@ class FailedBackupStatusPage extends HookConsumerWidget {
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints( constraints: const BoxConstraints(
minWidth: 100, minWidth: 100,
minHeight: 150, minHeight: 100,
maxWidth: 100, maxWidth: 100,
maxHeight: 200, maxHeight: 150,
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@ -95,9 +95,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
).toLocal(), ).toLocal(),
), ),
style: TextStyle( style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.grey[700], color: context.isDarkTheme
? Colors.white70
: Colors.grey[800],
), ),
), ),
Icon( Icon(
@ -115,7 +116,6 @@ class FailedBackupStatusPage extends HookConsumerWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12,
color: context.primaryColor, color: context.primaryColor,
), ),
), ),
@ -123,9 +123,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
Text( Text(
errorAsset.errorMessage, errorAsset.errorMessage,
style: TextStyle( style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.grey[800], color: context.isDarkTheme
? Colors.white70
: Colors.grey[800],
), ),
), ),
], ],

View file

@ -21,6 +21,8 @@ class User {
this.avatarColor = AvatarColorEnum.primary, this.avatarColor = AvatarColorEnum.primary,
this.memoryEnabled = true, this.memoryEnabled = true,
this.inTimeline = false, this.inTimeline = false,
this.quotaUsageInBytes = 0,
this.quotaSizeInBytes = 0,
}); });
Id get isarId => fastHash(id); Id get isarId => fastHash(id);
@ -36,7 +38,9 @@ class User {
isAdmin = dto.isAdmin, isAdmin = dto.isAdmin,
memoryEnabled = dto.memoriesEnabled ?? false, memoryEnabled = dto.memoriesEnabled ?? false,
avatarColor = dto.avatarColor.toAvatarColor(), avatarColor = dto.avatarColor.toAvatarColor(),
inTimeline = false; inTimeline = false,
quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,
quotaSizeInBytes = dto.quotaSizeInBytes ?? 0;
User.fromPartnerDto(PartnerResponseDto dto) User.fromPartnerDto(PartnerResponseDto dto)
: id = dto.id, : id = dto.id,
@ -49,7 +53,9 @@ class User {
isAdmin = dto.isAdmin, isAdmin = dto.isAdmin,
memoryEnabled = dto.memoriesEnabled ?? false, memoryEnabled = dto.memoriesEnabled ?? false,
avatarColor = dto.avatarColor.toAvatarColor(), 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 /// Base user dto used where the complete user object is not required
User.fromSimpleUserDto(UserDto dto) User.fromSimpleUserDto(UserDto dto)
@ -64,7 +70,9 @@ class User {
memoryEnabled = false, memoryEnabled = false,
isPartnerSharedBy = false, isPartnerSharedBy = false,
isPartnerSharedWith = false, isPartnerSharedWith = false,
updatedAt = DateTime.now(); updatedAt = DateTime.now(),
quotaUsageInBytes = 0,
quotaSizeInBytes = 0;
@Index(unique: true, replace: false, type: IndexType.hash) @Index(unique: true, replace: false, type: IndexType.hash)
String id; String id;
@ -79,7 +87,10 @@ class User {
AvatarColorEnum avatarColor; AvatarColorEnum avatarColor;
bool memoryEnabled; bool memoryEnabled;
bool inTimeline; bool inTimeline;
int quotaUsageInBytes;
int quotaSizeInBytes;
bool get hasQuota => quotaSizeInBytes > 0;
@Backlink(to: 'owner') @Backlink(to: 'owner')
final IsarLinks<Album> albums = IsarLinks<Album>(); final IsarLinks<Album> albums = IsarLinks<Album>();
@Backlink(to: 'sharedUsers') @Backlink(to: 'sharedUsers')
@ -98,7 +109,9 @@ class User {
profileImagePath == other.profileImagePath && profileImagePath == other.profileImagePath &&
isAdmin == other.isAdmin && isAdmin == other.isAdmin &&
memoryEnabled == other.memoryEnabled && memoryEnabled == other.memoryEnabled &&
inTimeline == other.inTimeline; inTimeline == other.inTimeline &&
quotaUsageInBytes == other.quotaUsageInBytes &&
quotaSizeInBytes == other.quotaSizeInBytes;
} }
@override @override
@ -114,7 +127,9 @@ class User {
avatarColor.hashCode ^ avatarColor.hashCode ^
isAdmin.hashCode ^ isAdmin.hashCode ^
memoryEnabled.hashCode ^ memoryEnabled.hashCode ^
inTimeline.hashCode; inTimeline.hashCode ^
quotaUsageInBytes.hashCode ^
quotaSizeInBytes.hashCode;
} }
enum AvatarColorEnum { enum AvatarColorEnum {

Binary file not shown.

View file

@ -3,18 +3,33 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.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/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
class CurrentUserProvider extends StateNotifier<User?> { class CurrentUserProvider extends StateNotifier<User?> {
CurrentUserProvider() : super(null) { CurrentUserProvider(this._apiService) : super(null) {
state = Store.tryGet(StoreKey.currentUser); state = Store.tryGet(StoreKey.currentUser);
streamSub = streamSub =
Store.watch(StoreKey.currentUser).listen((user) => state = user); Store.watch(StoreKey.currentUser).listen((user) => state = user);
} }
final ApiService _apiService;
late final StreamSubscription<User?> streamSub; late final StreamSubscription<User?> streamSub;
refresh() async {
try {
final user = await _apiService.userApi.getMyUserInfo();
if (user != null) {
Store.put(
StoreKey.currentUser,
User.fromUserDto(user),
);
}
} catch (_) {}
}
@override @override
void dispose() { void dispose() {
streamSub.cancel(); streamSub.cancel();
@ -24,7 +39,9 @@ class CurrentUserProvider extends StateNotifier<User?> {
final currentUserProvider = final currentUserProvider =
StateNotifierProvider<CurrentUserProvider, User?>((ref) { StateNotifierProvider<CurrentUserProvider, User?>((ref) {
return CurrentUserProvider(); return CurrentUserProvider(
ref.watch(apiServiceProvider),
);
}); });
class TimelineUserIdsProvider extends StateNotifier<List<int>> { class TimelineUserIdsProvider extends StateNotifier<List<int>> {

View file

@ -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_profile_info.dart';
import 'package:immich_mobile/shared/ui/app_bar_dialog/app_bar_server_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/shared/ui/confirm_dialog.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class ImmichAppBarDialog extends HookConsumerWidget { class ImmichAppBarDialog extends HookConsumerWidget {
@ -31,6 +32,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
useEffect( useEffect(
() { () {
ref.read(backupProvider.notifier).updateServerInfo(); ref.read(backupProvider.notifier).updateServerInfo();
ref.read(currentUserProvider.notifier).refresh();
return null; return null;
}, },
[user], [user],
@ -132,6 +134,16 @@ class ImmichAppBarDialog extends HookConsumerWidget {
} }
Widget buildStorageInformation() { 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( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 3),
child: Container( child: Container(
@ -163,7 +175,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: LinearProgressIndicator( child: LinearProgressIndicator(
minHeight: 5.0, minHeight: 5.0,
value: backupState.serverInfo.diskUsagePercentage / 100.0, value: percentage,
backgroundColor: Colors.grey, backgroundColor: Colors.grey,
color: theme.primaryColor, color: theme.primaryColor,
), ),
@ -173,8 +185,8 @@ class ImmichAppBarDialog extends HookConsumerWidget {
child: child:
const Text('backup_controller_page_storage_format').tr( const Text('backup_controller_page_storage_format').tr(
args: [ args: [
backupState.serverInfo.diskUse, usedDiskSpace,
backupState.serverInfo.diskSize, totalDiskSpace,
], ],
), ),
), ),

Binary file not shown.

Binary file not shown.

View file

@ -37,10 +37,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.9" version: "3.4.10"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -739,10 +739,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.3" version: "4.1.4"
image_picker: image_picker:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -8204,6 +8204,7 @@
}, },
"quotaUsageInBytes": { "quotaUsageInBytes": {
"format": "int64", "format": "int64",
"nullable": true,
"type": "integer" "type": "integer"
}, },
"shouldChangePassword": { "shouldChangePassword": {
@ -9912,6 +9913,7 @@
}, },
"quotaUsageInBytes": { "quotaUsageInBytes": {
"format": "int64", "format": "int64",
"nullable": true,
"type": "integer" "type": "integer"
}, },
"shouldChangePassword": { "shouldChangePassword": {

View file

@ -2496,7 +2496,7 @@ export interface PartnerResponseDto {
* @type {number} * @type {number}
* @memberof PartnerResponseDto * @memberof PartnerResponseDto
*/ */
'quotaUsageInBytes': number; 'quotaUsageInBytes': number | null;
/** /**
* *
* @type {boolean} * @type {boolean}
@ -4770,7 +4770,7 @@ export interface UserResponseDto {
* @type {number} * @type {number}
* @memberof UserResponseDto * @memberof UserResponseDto
*/ */
'quotaUsageInBytes': number; 'quotaUsageInBytes': number | null;
/** /**
* *
* @type {boolean} * @type {boolean}

View file

@ -36,7 +36,7 @@ export class UserResponseDto extends UserDto {
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes!: number | null; quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
quotaUsageInBytes!: number; quotaUsageInBytes!: number | null;
} }
export const mapSimpleUser = (entity: UserEntity): UserDto => { export const mapSimpleUser = (entity: UserEntity): UserDto => {