feat(backups): Update the models

pull/228/head
Inex Code 2023-06-29 12:52:09 +03:00
parent 02cb4dbf8b
commit d2d8add10d
25 changed files with 669 additions and 589 deletions

View File

@ -21,6 +21,7 @@ class HiveConfig {
Hive.registerAdapter(UserTypeAdapter());
Hive.registerAdapter(DnsProviderTypeAdapter());
Hive.registerAdapter(ServerProviderTypeAdapter());
Hive.registerAdapter(BackupsProviderTypeAdapter());
await Hive.openBox(BNames.appSettingsBox);

View File

@ -46,7 +46,7 @@ mutation ForceSnapshotsReload {
}
}
mutation StartBackup($serviceId: String = null) {
mutation StartBackup($serviceId: String!) {
backup {
startBackup(serviceId: $serviceId) {
...basicMutationReturnFields

View File

@ -1,9 +1,9 @@
import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$genericBackupConfigReturn {
Fragment$genericBackupConfigReturn({
@ -2738,31 +2738,27 @@ class _CopyWithStubImpl$Mutation$ForceSnapshotsReload$backup$forceSnapshotsReloa
}
class Variables$Mutation$StartBackup {
factory Variables$Mutation$StartBackup({String? serviceId}) =>
factory Variables$Mutation$StartBackup({required String serviceId}) =>
Variables$Mutation$StartBackup._({
if (serviceId != null) r'serviceId': serviceId,
r'serviceId': serviceId,
});
Variables$Mutation$StartBackup._(this._$data);
factory Variables$Mutation$StartBackup.fromJson(Map<String, dynamic> data) {
final result$data = <String, dynamic>{};
if (data.containsKey('serviceId')) {
final l$serviceId = data['serviceId'];
result$data['serviceId'] = (l$serviceId as String?);
}
final l$serviceId = data['serviceId'];
result$data['serviceId'] = (l$serviceId as String);
return Variables$Mutation$StartBackup._(result$data);
}
Map<String, dynamic> _$data;
String? get serviceId => (_$data['serviceId'] as String?);
String get serviceId => (_$data['serviceId'] as String);
Map<String, dynamic> toJson() {
final result$data = <String, dynamic>{};
if (_$data.containsKey('serviceId')) {
final l$serviceId = serviceId;
result$data['serviceId'] = l$serviceId;
}
final l$serviceId = serviceId;
result$data['serviceId'] = l$serviceId;
return result$data;
}
@ -2782,10 +2778,6 @@ class Variables$Mutation$StartBackup {
}
final l$serviceId = serviceId;
final lOther$serviceId = other.serviceId;
if (_$data.containsKey('serviceId') !=
other._$data.containsKey('serviceId')) {
return false;
}
if (l$serviceId != lOther$serviceId) {
return false;
}
@ -2795,8 +2787,7 @@ class Variables$Mutation$StartBackup {
@override
int get hashCode {
final l$serviceId = serviceId;
return Object.hashAll(
[_$data.containsKey('serviceId') ? l$serviceId : const {}]);
return Object.hashAll([l$serviceId]);
}
}
@ -2828,7 +2819,8 @@ class _CopyWithImpl$Variables$Mutation$StartBackup<TRes>
TRes call({Object? serviceId = _undefined}) =>
_then(Variables$Mutation$StartBackup._({
..._instance._$data,
if (serviceId != _undefined) 'serviceId': (serviceId as String?),
if (serviceId != _undefined && serviceId != null)
'serviceId': (serviceId as String),
}));
}
@ -2982,9 +2974,9 @@ const documentNodeMutationStartBackup = DocumentNode(definitions: [
variable: VariableNode(name: NameNode(value: 'serviceId')),
type: NamedTypeNode(
name: NameNode(value: 'String'),
isNonNull: false,
isNonNull: true,
),
defaultValue: DefaultValueNode(value: NullValueNode()),
defaultValue: DefaultValueNode(value: null),
directives: [],
)
],
@ -3052,7 +3044,7 @@ class Options$Mutation$StartBackup
extends graphql.MutationOptions<Mutation$StartBackup> {
Options$Mutation$StartBackup({
String? operationName,
Variables$Mutation$StartBackup? variables,
required Variables$Mutation$StartBackup variables,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
@ -3064,7 +3056,7 @@ class Options$Mutation$StartBackup
graphql.OnError? onError,
}) : onCompletedWithParsed = onCompleted,
super(
variables: variables?.toJson() ?? {},
variables: variables.toJson(),
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
@ -3098,7 +3090,7 @@ class WatchOptions$Mutation$StartBackup
extends graphql.WatchQueryOptions<Mutation$StartBackup> {
WatchOptions$Mutation$StartBackup({
String? operationName,
Variables$Mutation$StartBackup? variables,
required Variables$Mutation$StartBackup variables,
graphql.FetchPolicy? fetchPolicy,
graphql.ErrorPolicy? errorPolicy,
graphql.CacheRereadPolicy? cacheRereadPolicy,
@ -3110,7 +3102,7 @@ class WatchOptions$Mutation$StartBackup
bool carryForwardDataOnException = true,
bool fetchResults = false,
}) : super(
variables: variables?.toJson() ?? {},
variables: variables.toJson(),
operationName: operationName,
fetchPolicy: fetchPolicy,
errorPolicy: errorPolicy,
@ -3128,11 +3120,11 @@ class WatchOptions$Mutation$StartBackup
extension ClientExtension$Mutation$StartBackup on graphql.GraphQLClient {
Future<graphql.QueryResult<Mutation$StartBackup>> mutate$StartBackup(
[Options$Mutation$StartBackup? options]) async =>
await this.mutate(options ?? Options$Mutation$StartBackup());
Options$Mutation$StartBackup options) async =>
await this.mutate(options);
graphql.ObservableQuery<Mutation$StartBackup> watchMutation$StartBackup(
[WatchOptions$Mutation$StartBackup? options]) =>
this.watchMutation(options ?? WatchOptions$Mutation$StartBackup());
WatchOptions$Mutation$StartBackup options) =>
this.watchMutation(options);
}
class Mutation$StartBackup$backup {

View File

@ -3,7 +3,6 @@ import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({

View File

@ -19,6 +19,7 @@ type ApiDevice {
type ApiJob {
uid: String!
typeId: String!
name: String!
description: String!
status: String!

View File

@ -23,6 +23,7 @@ query GetApiJobs {
status
statusText
uid
typeId
updatedAt
}
}

View File

@ -1,9 +1,9 @@
import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart';
import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({
@ -2914,6 +2914,13 @@ const documentNodeQueryGetApiJobs = DocumentNode(definitions: [
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: 'typeId'),
alias: null,
arguments: [],
directives: [],
selectionSet: null,
),
FieldNode(
name: NameNode(value: 'updatedAt'),
alias: null,
@ -3229,6 +3236,7 @@ class Query$GetApiJobs$jobs$getJobs {
required this.status,
this.statusText,
required this.uid,
required this.typeId,
required this.updatedAt,
this.$__typename = 'ApiJob',
});
@ -3244,6 +3252,7 @@ class Query$GetApiJobs$jobs$getJobs {
final l$status = json['status'];
final l$statusText = json['statusText'];
final l$uid = json['uid'];
final l$typeId = json['typeId'];
final l$updatedAt = json['updatedAt'];
final l$$__typename = json['__typename'];
return Query$GetApiJobs$jobs$getJobs(
@ -3257,6 +3266,7 @@ class Query$GetApiJobs$jobs$getJobs {
status: (l$status as String),
statusText: (l$statusText as String?),
uid: (l$uid as String),
typeId: (l$typeId as String),
updatedAt: dateTimeFromJson(l$updatedAt),
$__typename: (l$$__typename as String),
);
@ -3282,6 +3292,8 @@ class Query$GetApiJobs$jobs$getJobs {
final String uid;
final String typeId;
final DateTime updatedAt;
final String $__typename;
@ -3309,6 +3321,8 @@ class Query$GetApiJobs$jobs$getJobs {
_resultData['statusText'] = l$statusText;
final l$uid = uid;
_resultData['uid'] = l$uid;
final l$typeId = typeId;
_resultData['typeId'] = l$typeId;
final l$updatedAt = updatedAt;
_resultData['updatedAt'] = dateTimeToJson(l$updatedAt);
final l$$__typename = $__typename;
@ -3328,6 +3342,7 @@ class Query$GetApiJobs$jobs$getJobs {
final l$status = status;
final l$statusText = statusText;
final l$uid = uid;
final l$typeId = typeId;
final l$updatedAt = updatedAt;
final l$$__typename = $__typename;
return Object.hashAll([
@ -3341,6 +3356,7 @@ class Query$GetApiJobs$jobs$getJobs {
l$status,
l$statusText,
l$uid,
l$typeId,
l$updatedAt,
l$$__typename,
]);
@ -3405,6 +3421,11 @@ class Query$GetApiJobs$jobs$getJobs {
if (l$uid != lOther$uid) {
return false;
}
final l$typeId = typeId;
final lOther$typeId = other.typeId;
if (l$typeId != lOther$typeId) {
return false;
}
final l$updatedAt = updatedAt;
final lOther$updatedAt = other.updatedAt;
if (l$updatedAt != lOther$updatedAt) {
@ -3448,6 +3469,7 @@ abstract class CopyWith$Query$GetApiJobs$jobs$getJobs<TRes> {
String? status,
String? statusText,
String? uid,
String? typeId,
DateTime? updatedAt,
String? $__typename,
});
@ -3477,6 +3499,7 @@ class _CopyWithImpl$Query$GetApiJobs$jobs$getJobs<TRes>
Object? status = _undefined,
Object? statusText = _undefined,
Object? uid = _undefined,
Object? typeId = _undefined,
Object? updatedAt = _undefined,
Object? $__typename = _undefined,
}) =>
@ -3504,6 +3527,9 @@ class _CopyWithImpl$Query$GetApiJobs$jobs$getJobs<TRes>
? _instance.statusText
: (statusText as String?),
uid: uid == _undefined || uid == null ? _instance.uid : (uid as String),
typeId: typeId == _undefined || typeId == null
? _instance.typeId
: (typeId as String),
updatedAt: updatedAt == _undefined || updatedAt == null
? _instance.updatedAt
: (updatedAt as DateTime),
@ -3530,6 +3556,7 @@ class _CopyWithStubImpl$Query$GetApiJobs$jobs$getJobs<TRes>
String? status,
String? statusText,
String? uid,
String? typeId,
DateTime? updatedAt,
String? $__typename,
}) =>

View File

@ -1,8 +1,8 @@
import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql;
import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql;
import 'package:selfprivacy/utils/scalars.dart';

View File

@ -1,8 +1,8 @@
import 'dart:async';
import 'disk_volumes.graphql.dart';
import 'package:gql/ast.dart';
import 'package:graphql/client.dart' as graphql;
import 'schema.graphql.dart';
import 'services.graphql.dart';
class Fragment$basicMutationReturnFields {
Fragment$basicMutationReturnFields({

View File

@ -0,0 +1,207 @@
part of 'server_api.dart';
mixin BackupsApi on GraphQLApiMap {
Future<List<Backup>> getBackups() async {
List<Backup> backups;
QueryResult<Query$AllBackupSnapshots> response;
try {
final GraphQLClient client = await getClient();
response = await client.query$AllBackupSnapshots();
if (response.hasException) {
final message = response.exception.toString();
print(message);
backups = [];
}
final List<Backup> parsed = response.parsedData!.backup.allSnapshots
.map(
(
final Query$AllBackupSnapshots$backup$allSnapshots snapshot,
) =>
Backup.fromGraphQL(snapshot),
)
.toList();
backups = parsed;
} catch (e) {
print(e);
backups = [];
}
return backups;
}
Future<BackupConfiguration?> getBackupsConfiguration() async {
BackupConfiguration? backupConfiguration;
QueryResult<Query$BackupConfiguration> response;
try {
final GraphQLClient client = await getClient();
response = await client.query$BackupConfiguration();
if (response.hasException) {
final message = response.exception.toString();
print(message);
backupConfiguration = null;
}
final BackupConfiguration parsed = BackupConfiguration.fromGraphQL(
response.parsedData!.backup.configuration,
);
backupConfiguration = parsed;
} catch (e) {
print(e);
backupConfiguration = null;
}
return backupConfiguration;
}
Future<GenericResult> forceBackupListReload() async {
try {
final GraphQLClient client = await getClient();
await client.mutate$ForceSnapshotsReload();
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return GenericResult(
success: true,
data: null,
);
}
Future<GenericResult> startBackup(final String serviceId) async {
QueryResult<Mutation$StartBackup> response;
GenericResult? result;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$StartBackup(serviceId: serviceId);
final options = Options$Mutation$StartBackup(variables: variables);
response = await client.mutate$StartBackup(options);
if (response.hasException) {
final message = response.exception.toString();
print(message);
result = GenericResult(
success: false,
data: null,
message: message,
);
}
result = GenericResult(
success: true,
data: null,
);
} catch (e) {
print(e);
result = GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return result;
}
Future<GenericResult> setAutobackupPeriod({final int? period}) async {
QueryResult<Mutation$SetAutobackupPeriod> response;
GenericResult? result;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$SetAutobackupPeriod(period: period);
final options =
Options$Mutation$SetAutobackupPeriod(variables: variables);
response = await client.mutate$SetAutobackupPeriod(options);
if (response.hasException) {
final message = response.exception.toString();
print(message);
result = GenericResult(
success: false,
data: null,
message: message,
);
}
result = GenericResult(
success: true,
data: null,
);
} catch (e) {
print(e);
result = GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return result;
}
Future<GenericResult> removeRepository() async {
try {
final GraphQLClient client = await getClient();
await client.mutate$RemoveRepository();
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return GenericResult(
success: true,
data: null,
);
}
Future<GenericResult> initializeRepository(
final InitializeRepositoryInput input,
) async {
QueryResult<Mutation$InitializeRepository> response;
GenericResult? result;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$InitializeRepository(
repository: Input$InitializeRepositoryInput(
locationId: input.locationId,
locationName: input.locationName,
login: input.login,
password: input.password,
provider: input.provider.toGraphQL(),
),
);
final options =
Options$Mutation$InitializeRepository(variables: variables);
response = await client.mutate$InitializeRepository(options);
if (response.hasException) {
final message = response.exception.toString();
print(message);
result = GenericResult(
success: false,
data: null,
message: message,
);
}
result = GenericResult(
success: true,
data: null,
);
} catch (e) {
print(e);
result = GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return result;
}
}

View File

@ -15,7 +15,7 @@ import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/initialize_repository_input.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/logic/models/json/backup.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/json/device_token.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
@ -32,9 +32,10 @@ part 'server_actions_api.dart';
part 'services_api.dart';
part 'users_api.dart';
part 'volume_api.dart';
part 'backups_api.dart';
class ServerApi extends GraphQLApiMap
with VolumeApi, JobsApi, ServerActionsApi, ServicesApi, UsersApi {
with VolumeApi, JobsApi, ServerActionsApi, ServicesApi, UsersApi, BackupsApi {
ServerApi({
this.hasLogger = false,
this.isWithToken = true,
@ -512,202 +513,4 @@ class ServerApi extends GraphQLApiMap
return token;
}
Future<GenericResult<List<Backup>>> getBackups() async {
GenericResult<List<Backup>> backups;
QueryResult<Query$AllBackupSnapshots> response;
try {
final GraphQLClient client = await getClient();
response = await client.query$AllBackupSnapshots();
if (response.hasException) {
final message = response.exception.toString();
print(message);
backups = GenericResult<List<Backup>>(
success: false,
data: [],
message: message,
);
}
final List<Backup> parsed = response.parsedData!.backup.allSnapshots
.map(
(
final Query$AllBackupSnapshots$backup$allSnapshots snapshot,
) =>
Backup.fromGraphQL(snapshot),
)
.toList();
backups = GenericResult<List<Backup>>(
success: true,
data: parsed,
);
} catch (e) {
print(e);
backups = GenericResult<List<Backup>>(
success: false,
data: [],
message: e.toString(),
);
}
return backups;
}
Future<GenericResult> forceBackupListReload() async {
try {
final GraphQLClient client = await getClient();
await client.mutate$ForceSnapshotsReload();
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return GenericResult(
success: true,
data: null,
);
}
Future<GenericResult> startBackup({final String? serviceId}) async {
QueryResult<Mutation$StartBackup> response;
GenericResult? result;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$StartBackup(serviceId: serviceId);
final options = Options$Mutation$StartBackup(variables: variables);
response = await client.mutate$StartBackup(options);
if (response.hasException) {
final message = response.exception.toString();
print(message);
result = GenericResult(
success: false,
data: null,
message: message,
);
}
result = GenericResult(
success: true,
data: null,
);
} catch (e) {
print(e);
result = GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return result;
}
Future<GenericResult> setAutobackupPeriod({final int? period}) async {
QueryResult<Mutation$SetAutobackupPeriod> response;
GenericResult? result;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$SetAutobackupPeriod(period: period);
final options =
Options$Mutation$SetAutobackupPeriod(variables: variables);
response = await client.mutate$SetAutobackupPeriod(options);
if (response.hasException) {
final message = response.exception.toString();
print(message);
result = GenericResult(
success: false,
data: null,
message: message,
);
}
result = GenericResult(
success: true,
data: null,
);
} catch (e) {
print(e);
result = GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return result;
}
Future<BackupStatus> getBackupStatus() async => BackupStatus(
progress: 0.0,
status: BackupStatusEnum.error,
errorMessage: null,
);
Future<GenericResult> removeRepository() async {
try {
final GraphQLClient client = await getClient();
await client.mutate$RemoveRepository();
} catch (e) {
print(e);
return GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return GenericResult(
success: true,
data: null,
);
}
Future<GenericResult> initializeRepository(
final InitializeRepositoryInput input,
) async {
QueryResult<Mutation$InitializeRepository> response;
GenericResult? result;
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$InitializeRepository(
repository: Input$InitializeRepositoryInput(
locationId: input.locationId,
locationName: input.locationName,
login: input.login,
password: input.password,
provider: input.provider.toGraphQL(),
),
);
final options =
Options$Mutation$InitializeRepository(variables: variables);
response = await client.mutate$InitializeRepository(options);
if (response.hasException) {
final message = response.exception.toString();
print(message);
result = GenericResult(
success: false,
data: null,
message: message,
);
}
result = GenericResult(
success: true,
data: null,
);
} catch (e) {
print(e);
result = GenericResult(
success: false,
data: null,
message: e.toString(),
);
}
return result;
}
}

View File

@ -6,7 +6,10 @@ import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/json/backup.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/initialize_repository_input.dart';
import 'package:selfprivacy/logic/models/service.dart';
part 'backups_state.dart';
@ -24,107 +27,83 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
if (bucket == null) {
emit(
const BackupsState(
isInitialized: false,
preventActions: false,
refreshing: false,
),
);
} else {
final BackupStatus status = await api.getBackupStatus();
switch (status.status) {
case BackupStatusEnum.noKey:
case BackupStatusEnum.notInitialized:
emit(
BackupsState(
backups: const [],
isInitialized: true,
preventActions: false,
progress: 0,
status: status.status,
refreshing: false,
),
);
break;
case BackupStatusEnum.initializing:
emit(
BackupsState(
backups: const [],
isInitialized: true,
preventActions: false,
progress: 0,
status: status.status,
refreshTimer: const Duration(seconds: 10),
refreshing: false,
),
);
break;
case BackupStatusEnum.initialized:
case BackupStatusEnum.error:
final result = await api.getBackups();
emit(
BackupsState(
backups: result.data,
isInitialized: true,
preventActions: false,
progress: status.progress,
status: status.status,
error: status.errorMessage ?? '',
refreshing: false,
),
);
break;
case BackupStatusEnum.backingUp:
case BackupStatusEnum.restoring:
final result = await api.getBackups();
emit(
BackupsState(
backups: result.data,
isInitialized: true,
preventActions: true,
progress: status.progress,
status: status.status,
error: status.errorMessage ?? '',
refreshTimer: const Duration(seconds: 5),
refreshing: false,
),
);
break;
default:
emit(const BackupsState());
}
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
}
final BackupConfiguration? backupConfig =
await api.getBackupsConfiguration();
final List<Backup> backups = await api.getBackups();
emit(
state.copyWith(
backblazeBucket: bucket,
isInitialized: backupConfig?.isInitialized,
autobackupPeriod: backupConfig?.autobackupPeriod,
backups: backups,
preventActions: false,
refreshing: false,
),
);
print(state);
}
}
Future<void> createBucket() async {
Future<void> initializeBackups() async {
emit(state.copyWith(preventActions: true));
final String domain = serverInstallationCubit.state.serverDomain!.domainName
.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
final int serverId = serverInstallationCubit.state.serverDetails!.id;
String bucketName = 'selfprivacy-$domain-$serverId';
// If bucket name is too long, shorten it
if (bucketName.length > 49) {
bucketName = bucketName.substring(0, 49);
final String? encryptionKey =
(await api.getBackupsConfiguration())?.encryptionKey;
if (encryptionKey == null) {
getIt<NavigationService>()
.showSnackBar("Couldn't get encryption key from your server.");
emit(state.copyWith(preventActions: false));
return;
}
final String bucketId = await backblaze.createBucket(bucketName);
final BackblazeApplicationKey key = await backblaze.createKey(bucketId);
final BackblazeBucket bucket = BackblazeBucket(
bucketId: bucketId,
bucketName: bucketName,
applicationKey: key.applicationKey,
applicationKeyId: key.applicationKeyId,
final BackblazeBucket bucket;
if (state.backblazeBucket == null) {
final String domain = serverInstallationCubit
.state.serverDomain!.domainName
.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
final int serverId = serverInstallationCubit.state.serverDetails!.id;
String bucketName = 'selfprivacy-$domain-$serverId';
// If bucket name is too long, shorten it
if (bucketName.length > 49) {
bucketName = bucketName.substring(0, 49);
}
final String bucketId = await backblaze.createBucket(bucketName);
final BackblazeApplicationKey key = await backblaze.createKey(bucketId);
bucket = BackblazeBucket(
bucketId: bucketId,
bucketName: bucketName,
applicationKey: key.applicationKey,
applicationKeyId: key.applicationKeyId,
encryptionKey: encryptionKey,
);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
emit(state.copyWith(backblazeBucket: bucket));
} else {
bucket = state.backblazeBucket!;
}
final GenericResult result = await api.initializeRepository(
InitializeRepositoryInput(
provider: BackupsProviderType.backblaze,
locationId: bucket.bucketId,
locationName: bucket.bucketName,
login: bucket.applicationKeyId,
password: bucket.applicationKey,
),
);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
//await api.uploadBackblazeConfig(bucket);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
return;
}
await updateBackups();
getIt<NavigationService>().showSnackBar(
'Backups repository is now initializing. It may take a while.');
emit(state.copyWith(isInitialized: true, preventActions: false));
emit(state.copyWith(preventActions: false));
}
Future<void> reuploadKey() async {
@ -132,42 +111,47 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
if (bucket == null) {
emit(state.copyWith(isInitialized: false));
print('bucket is null');
} else {
//await api.uploadBackblazeConfig(bucket);
emit(state.copyWith(isInitialized: true, preventActions: false));
getIt<NavigationService>().showSnackBar('backup.reuploaded_key');
print('bucket is not null');
final GenericResult result = await api.initializeRepository(
InitializeRepositoryInput(
provider: BackupsProviderType.backblaze,
locationId: bucket.bucketId,
locationName: bucket.bucketName,
login: bucket.applicationKeyId,
password: bucket.applicationKey,
),
);
print('result is $result');
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
return;
} else {
emit(state.copyWith(preventActions: false));
getIt<NavigationService>().showSnackBar('backup.reuploaded_key');
await updateBackups();
}
}
}
Duration refreshTimeFromState(final BackupStatusEnum status) {
switch (status) {
case BackupStatusEnum.backingUp:
case BackupStatusEnum.restoring:
return const Duration(seconds: 5);
case BackupStatusEnum.initializing:
return const Duration(seconds: 10);
default:
return const Duration(seconds: 60);
}
}
@Deprecated("we don't have states")
Duration refreshTimeFromState() => const Duration(seconds: 60);
Future<void> updateBackups({final bool useTimer = false}) async {
emit(state.copyWith(refreshing: true));
final result = await api.getBackups();
if (!result.success || result.data.isEmpty) {
return;
}
final backups = await api.getBackups();
final backupConfig = await api.getBackupsConfiguration();
final List<Backup> backups = result.data;
final BackupStatus status = await api.getBackupStatus();
emit(
state.copyWith(
backups: backups,
progress: status.progress,
status: status.status,
error: status.errorMessage,
refreshTimer: refreshTimeFromState(status.status),
refreshTimer: refreshTimeFromState(),
refreshing: false,
isInitialized: backupConfig?.isInitialized ?? false,
autobackupPeriod: backupConfig?.autobackupPeriod,
),
);
if (useTimer) {
@ -182,9 +166,18 @@ class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
emit(state.copyWith(preventActions: false));
}
Future<void> createBackup() async {
Future<void> createMultipleBackups(final List<Service> services) async {
emit(state.copyWith(preventActions: true));
await api.startBackup();
for (final service in services) {
await api.startBackup(service.id);
}
await updateBackups();
emit(state.copyWith(preventActions: false));
}
Future<void> createBackup(final String serviceId) async {
emit(state.copyWith(preventActions: true));
await api.startBackup(serviceId);
await updateBackups();
emit(state.copyWith(preventActions: false));
}

View File

@ -4,31 +4,26 @@ class BackupsState extends ServerInstallationDependendState {
const BackupsState({
this.isInitialized = false,
this.backups = const [],
this.progress = 0.0,
this.status = BackupStatusEnum.noKey,
this.preventActions = true,
this.error = '',
this.refreshTimer = const Duration(seconds: 60),
this.refreshing = true,
this.autobackupPeriod,
this.backblazeBucket,
});
final bool isInitialized;
final List<Backup> backups;
final double progress;
final BackupStatusEnum status;
final bool preventActions;
final String error;
final Duration refreshTimer;
final bool refreshing;
final Duration? autobackupPeriod;
final BackblazeBucket? backblazeBucket;
@override
List<Object> get props => [
isInitialized,
backups,
progress,
preventActions,
status,
error,
refreshTimer,
refreshing
];
@ -36,21 +31,19 @@ class BackupsState extends ServerInstallationDependendState {
BackupsState copyWith({
final bool? isInitialized,
final List<Backup>? backups,
final double? progress,
final BackupStatusEnum? status,
final bool? preventActions,
final String? error,
final Duration? refreshTimer,
final bool? refreshing,
final Duration? autobackupPeriod,
final BackblazeBucket? backblazeBucket,
}) =>
BackupsState(
isInitialized: isInitialized ?? this.isInitialized,
backups: backups ?? this.backups,
progress: progress ?? this.progress,
status: status ?? this.status,
preventActions: preventActions ?? this.preventActions,
error: error ?? this.error,
refreshTimer: refreshTimer ?? this.refreshTimer,
refreshing: refreshing ?? this.refreshing,
autobackupPeriod: autobackupPeriod ?? this.autobackupPeriod,
backblazeBucket: backblazeBucket ?? this.backblazeBucket,
);
}

View File

@ -197,7 +197,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
final BackupsCredential backblazeCredential = BackupsCredential(
keyId: keyId,
applicationKey: applicationKey,
provider: BackupsProvider.backblaze,
provider: BackupsProviderType.backblaze,
);
await repository.saveBackblazeKey(backblazeCredential);
if (state is ServerInstallationRecovery) {
@ -699,7 +699,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
provider: dnsProviderType,
),
);
await repository.setDnsApiToken(token);
// await repository.setDnsApiToken(token);
emit(
dataState.copyWith(
serverDomain: ServerDomain(

View File

@ -0,0 +1,60 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/backups.graphql.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
class Backup {
Backup.fromGraphQL(
final Query$AllBackupSnapshots$backup$allSnapshots snapshot,
) : this(
id: snapshot.id,
time: snapshot.createdAt,
serviceId: snapshot.service.id,
fallbackServiceName: snapshot.service.displayName,
);
Backup({
required this.time,
required this.id,
required this.serviceId,
required this.fallbackServiceName,
});
// Time of the backup
final DateTime time;
@JsonKey(name: 'short_id')
final String id;
final String serviceId;
final String fallbackServiceName;
}
class BackupConfiguration {
BackupConfiguration.fromGraphQL(
final Query$BackupConfiguration$backup$configuration configuration,
) : this(
// Provided by API as int of minutes
autobackupPeriod: configuration.autobackupPeriod != null
? Duration(minutes: configuration.autobackupPeriod!)
: null,
encryptionKey: configuration.encryptionKey,
isInitialized: configuration.isInitialized,
locationId: configuration.locationId,
locationName: configuration.locationName,
provider: BackupsProviderType.fromGraphQL(configuration.provider),
);
BackupConfiguration({
required this.autobackupPeriod,
required this.encryptionKey,
required this.isInitialized,
required this.locationId,
required this.locationName,
required this.provider,
});
final Duration? autobackupPeriod;
final String encryptionKey;
final bool isInitialized;
final String? locationId;
final String? locationName;
final BackupsProviderType provider;
}

View File

@ -9,6 +9,7 @@ class BackblazeBucket {
required this.bucketName,
required this.applicationKeyId,
required this.applicationKey,
required this.encryptionKey,
});
@HiveField(0)
@ -23,6 +24,9 @@ class BackblazeBucket {
@HiveField(3)
final String bucketName;
@HiveField(4)
final String encryptionKey;
@override
String toString() => bucketName;
}

View File

@ -21,13 +21,14 @@ class BackblazeBucketAdapter extends TypeAdapter<BackblazeBucket> {
bucketName: fields[3] as String,
applicationKeyId: fields[1] as String,
applicationKey: fields[2] as String,
encryptionKey: fields[4] as String,
);
}
@override
void write(BinaryWriter writer, BackblazeBucket obj) {
writer
..writeByte(4)
..writeByte(5)
..writeByte(0)
..write(obj.bucketId)
..writeByte(1)
@ -35,7 +36,9 @@ class BackblazeBucketAdapter extends TypeAdapter<BackblazeBucket> {
..writeByte(2)
..write(obj.applicationKey)
..writeByte(3)
..write(obj.bucketName);
..write(obj.bucketName)
..writeByte(4)
..write(obj.encryptionKey);
}
@override

View File

@ -19,8 +19,8 @@ class BackupsCredential {
@HiveField(1)
final String applicationKey;
@HiveField(2, defaultValue: BackupsProvider.backblaze)
final BackupsProvider provider;
@HiveField(2, defaultValue: BackupsProviderType.backblaze)
final BackupsProviderType provider;
String get encodedApiKey => encodedBackblazeKey(keyId, applicationKey);
@ -35,7 +35,7 @@ String encodedBackblazeKey(final String? keyId, final String? applicationKey) {
}
@HiveType(typeId: 103)
enum BackupsProvider {
enum BackupsProviderType {
@HiveField(0)
none,
@HiveField(1)
@ -45,7 +45,7 @@ enum BackupsProvider {
@HiveField(3)
backblaze;
factory BackupsProvider.fromGraphQL(final Enum$BackupProvider provider) =>
factory BackupsProviderType.fromGraphQL(final Enum$BackupProvider provider) =>
switch (provider) {
Enum$BackupProvider.NONE => none,
Enum$BackupProvider.MEMORY => memory,

View File

@ -20,8 +20,8 @@ class BackupsCredentialAdapter extends TypeAdapter<BackupsCredential> {
keyId: fields[0] as String,
applicationKey: fields[1] as String,
provider: fields[2] == null
? BackupsProvider.backblaze
: fields[2] as BackupsProvider,
? BackupsProviderType.backblaze
: fields[2] as BackupsProviderType,
);
}
@ -48,39 +48,39 @@ class BackupsCredentialAdapter extends TypeAdapter<BackupsCredential> {
typeId == other.typeId;
}
class BackupsProviderAdapter extends TypeAdapter<BackupsProvider> {
class BackupsProviderTypeAdapter extends TypeAdapter<BackupsProviderType> {
@override
final int typeId = 103;
@override
BackupsProvider read(BinaryReader reader) {
BackupsProviderType read(BinaryReader reader) {
switch (reader.readByte()) {
case 0:
return BackupsProvider.none;
return BackupsProviderType.none;
case 1:
return BackupsProvider.memory;
return BackupsProviderType.memory;
case 2:
return BackupsProvider.file;
return BackupsProviderType.file;
case 3:
return BackupsProvider.backblaze;
return BackupsProviderType.backblaze;
default:
return BackupsProvider.none;
return BackupsProviderType.none;
}
}
@override
void write(BinaryWriter writer, BackupsProvider obj) {
void write(BinaryWriter writer, BackupsProviderType obj) {
switch (obj) {
case BackupsProvider.none:
case BackupsProviderType.none:
writer.writeByte(0);
break;
case BackupsProvider.memory:
case BackupsProviderType.memory:
writer.writeByte(1);
break;
case BackupsProvider.file:
case BackupsProviderType.file:
writer.writeByte(2);
break;
case BackupsProvider.backblaze:
case BackupsProviderType.backblaze:
writer.writeByte(3);
break;
}
@ -92,7 +92,7 @@ class BackupsProviderAdapter extends TypeAdapter<BackupsProvider> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is BackupsProviderAdapter &&
other is BackupsProviderTypeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -8,7 +8,7 @@ class InitializeRepositoryInput {
required this.login,
required this.password,
});
final BackupsProvider provider;
final BackupsProviderType provider;
final String locationId;
final String locationName;
final String login;

View File

@ -1,65 +0,0 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/backups.graphql.dart';
part 'backup.g.dart';
@JsonSerializable()
class Backup {
factory Backup.fromJson(final Map<String, dynamic> json) =>
_$BackupFromJson(json);
Backup.fromGraphQL(
final Query$AllBackupSnapshots$backup$allSnapshots snapshot,
) : this(
id: snapshot.id,
time: snapshot.createdAt,
serviceId: snapshot.service.id,
fallbackServiceName: snapshot.service.displayName,
);
Backup({
required this.time,
required this.id,
required this.serviceId,
required this.fallbackServiceName,
});
// Time of the backup
final DateTime time;
@JsonKey(name: 'short_id')
final String id;
final String serviceId;
final String fallbackServiceName;
}
enum BackupStatusEnum {
@JsonValue('NO_KEY')
noKey,
@JsonValue('NOT_INITIALIZED')
notInitialized,
@JsonValue('INITIALIZED')
initialized,
@JsonValue('BACKING_UP')
backingUp,
@JsonValue('RESTORING')
restoring,
@JsonValue('ERROR')
error,
@JsonValue('INITIALIZING')
initializing,
}
@JsonSerializable()
class BackupStatus {
factory BackupStatus.fromJson(final Map<String, dynamic> json) =>
_$BackupStatusFromJson(json);
BackupStatus({
required this.status,
required this.progress,
required this.errorMessage,
});
final BackupStatusEnum status;
final double progress;
@JsonKey(name: 'error_message')
final String? errorMessage;
}

View File

@ -1,44 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Backup _$BackupFromJson(Map<String, dynamic> json) => Backup(
time: DateTime.parse(json['time'] as String),
id: json['short_id'] as String,
serviceId: json['serviceId'] as String,
fallbackServiceName: json['fallbackServiceName'] as String,
);
Map<String, dynamic> _$BackupToJson(Backup instance) => <String, dynamic>{
'time': instance.time.toIso8601String(),
'short_id': instance.id,
'serviceId': instance.serviceId,
'fallbackServiceName': instance.fallbackServiceName,
};
BackupStatus _$BackupStatusFromJson(Map<String, dynamic> json) => BackupStatus(
status: $enumDecode(_$BackupStatusEnumEnumMap, json['status']),
progress: (json['progress'] as num).toDouble(),
errorMessage: json['error_message'] as String?,
);
Map<String, dynamic> _$BackupStatusToJson(BackupStatus instance) =>
<String, dynamic>{
'status': _$BackupStatusEnumEnumMap[instance.status]!,
'progress': instance.progress,
'error_message': instance.errorMessage,
};
const _$BackupStatusEnumEnumMap = {
BackupStatusEnum.noKey: 'NO_KEY',
BackupStatusEnum.notInitialized: 'NOT_INITIALIZED',
BackupStatusEnum.initialized: 'INITIALIZED',
BackupStatusEnum.backingUp: 'BACKING_UP',
BackupStatusEnum.restoring: 'RESTORING',
BackupStatusEnum.error: 'ERROR',
BackupStatusEnum.initializing: 'INITIALIZING',
};

View File

@ -1,9 +1,14 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/models/json/backup.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/outlined_card.dart';
@ -29,19 +34,15 @@ class _BackupDetailsPageState extends State<BackupDetailsPage>
is ServerInstallationFinished;
final bool isBackupInitialized =
context.watch<BackupsCubit>().state.isInitialized;
final BackupStatusEnum backupStatus =
context.watch<BackupsCubit>().state.status;
final StateType providerState = isReady && isBackupInitialized
? (backupStatus == BackupStatusEnum.error
? StateType.warning
: StateType.stable)
? StateType.stable
: StateType.uninitialized;
final bool preventActions =
context.watch<BackupsCubit>().state.preventActions;
final double backupProgress = context.watch<BackupsCubit>().state.progress;
final String backupError = context.watch<BackupsCubit>().state.error;
final List<Backup> backups = context.watch<BackupsCubit>().state.backups;
final bool refreshing = context.watch<BackupsCubit>().state.refreshing;
final List<Service> services =
context.watch<ServicesCubit>().state.services;
return BrandHeroScreen(
heroIcon: BrandIcons.save,
@ -53,81 +54,45 @@ class _BackupDetailsPageState extends State<BackupDetailsPage>
onPressed: preventActions
? null
: () async {
await context.read<BackupsCubit>().createBucket();
await context.read<BackupsCubit>().initializeBackups();
},
text: 'backup.initialize'.tr(),
),
if (backupStatus == BackupStatusEnum.initializing)
Text(
'backup.waiting_for_rebuild'.tr(),
style: Theme.of(context).textTheme.bodyMedium,
),
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
OutlinedCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (backupStatus == BackupStatusEnum.initialized)
ListTile(
onTap: preventActions
? null
: () async {
await context.read<BackupsCubit>().createBackup();
},
leading: const Icon(
Icons.add_circle_outline_rounded,
),
title: Text(
'backup.create_new'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
),
if (backupStatus == BackupStatusEnum.backingUp)
ListTile(
title: Text(
'backup.creating'.tr(
args: [(backupProgress * 100).round().toString()],
ListTile(
onTap: preventActions
? null
: () {
// await context.read<BackupsCubit>().createBackup();
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder: (final BuildContext context) =>
DraggableScrollableSheet(
expand: false,
maxChildSize: 0.9,
minChildSize: 0.4,
initialChildSize: 0.6,
builder: (context, scrollController) =>
CreateBackupsModal(
services: services,
scrollController: scrollController,
),
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: LinearProgressIndicator(
value: backupProgress,
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.restoring)
ListTile(
title: Text(
'backup.restoring'.tr(
args: [(backupProgress * 100).round().toString()],
),
style: Theme.of(context).textTheme.titleLarge,
),
subtitle: LinearProgressIndicator(
backgroundColor: Colors.grey.withOpacity(0.2),
),
),
if (backupStatus == BackupStatusEnum.error)
ListTile(
leading: Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.error,
),
title: Text(
'backup.error_pending'.tr(),
style: Theme.of(context).textTheme.titleLarge,
),
),
],
),
);
},
leading: const Icon(
Icons.add_circle_outline_rounded,
),
title: Text(
'backup.create_new'.tr(),
),
),
const SizedBox(height: 16),
// Card with a list of existing backups
// Each list item has a date
// When clicked, starts the restore action
if (backupStatus != BackupStatusEnum.initializing &&
backupStatus != BackupStatusEnum.noKey)
if (isBackupInitialized)
OutlinedCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -230,11 +195,151 @@ class _BackupDetailsPageState extends State<BackupDetailsPage>
],
),
),
if (backupStatus == BackupStatusEnum.error)
Text(
backupError.toString(),
style: Theme.of(context).textTheme.bodyMedium,
),
],
);
}
}
class CreateBackupsModal extends StatefulWidget {
const CreateBackupsModal({
super.key,
required this.services,
required this.scrollController,
});
final List<Service> services;
final ScrollController scrollController;
@override
State<CreateBackupsModal> createState() => _CreateBackupsModalState();
}
class _CreateBackupsModalState extends State<CreateBackupsModal> {
// Store in state the selected services to backup
List<Service> selectedServices = [];
// Select all services on modal open
@override
void initState() {
super.initState();
final List<String> busyServices = context
.read<ServerJobsCubit>()
.state
.backupJobList
.where((final ServerJob job) =>
job.status == JobStatusEnum.running ||
job.status == JobStatusEnum.created)
.map((final ServerJob job) => job.typeId.split('.')[1])
.toList();
selectedServices.addAll(widget.services
.where((final Service service) => !busyServices.contains(service.id)));
}
@override
Widget build(final BuildContext context) {
final List<String> busyServices = context
.watch<ServerJobsCubit>()
.state
.backupJobList
.where((final ServerJob job) =>
job.status == JobStatusEnum.running ||
job.status == JobStatusEnum.created)
.map((final ServerJob job) => job.typeId.split('.')[1])
.toList();
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 16),
Text(
'backup.create_new_select_headline'.tr(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// Select all services tile
CheckboxListTile(
onChanged: (final bool? value) {
setState(() {
if (value ?? true) {
setState(() {
selectedServices.clear();
selectedServices.addAll(widget.services.where(
(final service) => !busyServices.contains(service.id)));
});
} else {
selectedServices.clear();
}
});
},
title: Text(
'backup.select_all'.tr(),
),
secondary: const Icon(
Icons.checklist_outlined,
),
value: selectedServices.length >=
widget.services.length - busyServices.length,
),
const Divider(
height: 1.0,
),
...widget.services.map(
(final Service service) {
final bool busy = busyServices.contains(service.id);
return CheckboxListTile(
onChanged: !busy
? (final bool? value) {
setState(() {
if (value ?? true) {
setState(() {
selectedServices.add(service);
});
} else {
setState(() {
selectedServices.remove(service);
});
}
});
}
: null,
title: Text(
service.displayName,
),
subtitle: Text(
busy ? 'backup.service_busy'.tr() : service.description,
),
secondary: SvgPicture.string(
service.svgIcon,
height: 24,
width: 24,
colorFilter: ColorFilter.mode(
busy
? Theme.of(context).colorScheme.outlineVariant
: Theme.of(context).colorScheme.onBackground,
BlendMode.srcIn,
),
),
value: selectedServices.contains(service),
);
},
),
const SizedBox(height: 16),
// Create backup button
FilledButton(
onPressed: selectedServices.isEmpty
? null
: () {
context
.read<BackupsCubit>()
.createMultipleBackups(selectedServices);
Navigator.of(context).pop();
},
child: Text(
'backup.create'.tr(),
),
),
],
);
}

View File

@ -97,16 +97,15 @@ class _ProvidersPageState extends State<ProvidersPage> {
),
const SizedBox(height: 16),
// TODO: When backups are fixed, show this card
if (isBackupInitialized)
_Card(
state: isBackupInitialized
? StateType.stable
: StateType.uninitialized,
icon: BrandIcons.save,
title: 'backup.card_title'.tr(),
subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '',
onTap: () => context.pushRoute(const BackupDetailsRoute()),
),
_Card(
state: isBackupInitialized
? StateType.stable
: StateType.uninitialized,
icon: BrandIcons.save,
title: 'backup.card_title'.tr(),
subtitle: isBackupInitialized ? 'backup.card_subtitle'.tr() : '',
onTap: () => context.pushRoute(const BackupDetailsRoute()),
),
],
),
);