forked from dark_thunder/immich
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:
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"flutterSdkVersion": "3.13.0",
|
||||
"flavors": {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"flutter": "3.13.6"
|
||||
}
|
||||
+4
-2
@@ -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
|
||||
libisar.so
|
||||
|
||||
# FVM Version Cache
|
||||
.fvm/
|
||||
Vendored
+1
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -179,4 +179,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
COCOAPODS: 1.11.3
|
||||
|
||||
@@ -81,6 +81,7 @@ Future<void> 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<Album> albums = IsarLinks<Album>();
|
||||
@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 {
|
||||
|
||||
Generated
+284
-35
File diff suppressed because it is too large
Load Diff
@@ -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<User?> {
|
||||
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<User?> 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<User?> {
|
||||
|
||||
final currentUserProvider =
|
||||
StateNotifierProvider<CurrentUserProvider, User?>((ref) {
|
||||
return CurrentUserProvider();
|
||||
return CurrentUserProvider(
|
||||
ref.watch(apiServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
class TimelineUserIdsProvider extends StateNotifier<List<int>> {
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+7
-3
@@ -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<String>(json, r'oauthId')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes')!,
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
|
||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', '')!,
|
||||
|
||||
+7
-3
@@ -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<String>(json, r'oauthId')!,
|
||||
profileImagePath: mapValueOfType<String>(json, r'profileImagePath')!,
|
||||
quotaSizeInBytes: mapValueOfType<int>(json, r'quotaSizeInBytes'),
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes')!,
|
||||
quotaUsageInBytes: mapValueOfType<int>(json, r'quotaUsageInBytes'),
|
||||
shouldChangePassword: mapValueOfType<bool>(json, r'shouldChangePassword')!,
|
||||
storageLabel: mapValueOfType<String>(json, r'storageLabel'),
|
||||
updatedAt: mapDateTime(json, r'updatedAt', '')!,
|
||||
|
||||
+4
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user