Merge pull request 'refactor: Introduce the API connection repository' (#440) from api-connection-refactor into master
continuous-integration/drone/push Build was killed Details

Reviewed-on: #440
Reviewed-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
pull/467/head
Inex Code 2024-02-23 16:49:39 +02:00
commit 0d12b1d2d7
116 changed files with 6134 additions and 2372 deletions

View File

@ -36,7 +36,8 @@
"continue": "Continue",
"alert": "Alert",
"copied_to_clipboard": "Copied to clipboard!",
"please_connect": "Please connect your server, domain and DNS provider to dive in!"
"please_connect": "Please connect your server, domain and DNS provider to dive in!",
"network_error": "Network error"
},
"more_page": {
"configuration_wizard": "Setup wizard",
@ -305,6 +306,10 @@
"extending_volume_description": "Resizing volume will allow you to store more data on your server without extending the server itself. Volume can only be extended: shrinking is not possible.",
"extending_volume_price_info": "Price includes VAT and is estimated from pricing data provided by your server provider. Server will be rebooted after resizing.",
"extending_volume_error": "Couldn't initialize volume extending.",
"extending_volume_started": "Volume extending started",
"extending_volume_provider_waiting": "Provider volume resized, waiting 10 seconds…",
"extending_volume_server_waiting": "Server volume resized, waiting 20 seconds…",
"extending_volume_rebooting": "Rebooting server…",
"extending_volume_modal_description": "Upgrade to {} for {} plan per month.",
"size": "Size",
"price": "Price",
@ -390,7 +395,8 @@
"could_not_add_ssh_key": "Couldn't add SSH key",
"username_rule": "Username must contain only lowercase latin letters, digits and underscores, should not start with a digit",
"email_login": "Email login",
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon."
"no_ssh_notice": "Only email and SSH accounts are created for this user. Single Sign On for all services is coming soon.",
"user_already_exists": "User with such username already exists"
},
"initializing": {
"server_provider_description": "A place where your data and SelfPrivacy services will reside:",
@ -590,6 +596,8 @@
"service_turn_off": "Turn off",
"service_turn_on": "Turn on",
"job_added": "Job added",
"job_postponed": "Job added, but you will be able to launch it after current jobs are finished",
"job_removed": "Job removed",
"run_jobs": "Run jobs",
"reboot_success": "Server is rebooting",
"reboot_failed": "Couldn't reboot the server. Check the app logs.",
@ -602,7 +610,11 @@
"delete_ssh_key": "Delete SSH key for {}",
"server_jobs": "Jobs on the server",
"reset_user_password": "Reset password of user",
"generic_error": "Couldn't connect to the server!"
"generic_error": "Couldn't connect to the server!",
"rebuild_system": "Rebuild system",
"start_server_upgrade": "Start the server upgrade",
"change_auto_upgrade_settings": "Change auto-upgrade settings",
"change_server_timezone": "Change server timezone"
},
"validations": {
"required": "Required",
@ -634,4 +646,4 @@
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses"
}
}
}

View File

@ -1,43 +1,64 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/connection_status/connection_status_bloc.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/support_system/support_system_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
class BlocAndProviderConfig extends StatelessWidget {
class BlocAndProviderConfig extends StatefulWidget {
const BlocAndProviderConfig({super.key, this.child});
final Widget? child;
@override
BlocAndProviderConfigState createState() => BlocAndProviderConfigState();
}
class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
late final ServerInstallationCubit serverInstallationCubit;
late final SupportSystemCubit supportSystemCubit;
late final UsersBloc usersBloc;
late final ServicesBloc servicesBloc;
late final BackupsBloc backupsBloc;
late final DnsRecordsCubit dnsRecordsCubit;
late final RecoveryKeyBloc recoveryKeyBloc;
late final DevicesBloc devicesBloc;
late final ServerJobsBloc serverJobsBloc;
late final ConnectionStatusBloc connectionStatusBloc;
late final ServerDetailsCubit serverDetailsCubit;
late final VolumesBloc volumesBloc;
@override
void initState() {
super.initState();
serverInstallationCubit = ServerInstallationCubit()..load();
supportSystemCubit = SupportSystemCubit();
usersBloc = UsersBloc();
servicesBloc = ServicesBloc();
backupsBloc = BackupsBloc();
dnsRecordsCubit = DnsRecordsCubit();
recoveryKeyBloc = RecoveryKeyBloc();
devicesBloc = DevicesBloc();
serverJobsBloc = ServerJobsBloc();
connectionStatusBloc = ConnectionStatusBloc();
serverDetailsCubit = ServerDetailsCubit();
volumesBloc = VolumesBloc();
}
@override
Widget build(final BuildContext context) {
const isDark = false;
const isAutoDark = true;
final serverInstallationCubit = ServerInstallationCubit()..load();
final supportSystemCubit = SupportSystemCubit();
final usersCubit = UsersCubit(serverInstallationCubit);
final servicesCubit = ServicesCubit(serverInstallationCubit);
final backupsCubit = BackupsCubit(serverInstallationCubit);
final dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit);
final recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit);
final apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit);
final apiVolumesCubit = ApiProviderVolumeCubit(serverInstallationCubit);
final apiServerVolumesCubit =
ApiServerVolumeCubit(serverInstallationCubit, apiVolumesCubit);
final serverJobsCubit = ServerJobsCubit(serverInstallationCubit);
final serverDetailsCubit = ServerDetailsCubit(serverInstallationCubit);
return MultiProvider(
providers: [
@ -56,49 +77,37 @@ class BlocAndProviderConfig extends StatelessWidget {
lazy: false,
),
BlocProvider(
create: (final _) => ProvidersCubit(),
),
BlocProvider(
create: (final _) => usersCubit..load(),
create: (final _) => usersBloc,
lazy: false,
),
BlocProvider(
create: (final _) => servicesCubit..load(),
lazy: false,
create: (final _) => servicesBloc,
),
BlocProvider(
create: (final _) => backupsCubit..load(),
lazy: false,
create: (final _) => backupsBloc,
),
BlocProvider(
create: (final _) => dnsRecordsCubit..load(),
create: (final _) => dnsRecordsCubit,
),
BlocProvider(
create: (final _) => recoveryKeyCubit..load(),
create: (final _) => recoveryKeyBloc,
),
BlocProvider(
create: (final _) => apiDevicesCubit..load(),
create: (final _) => devicesBloc,
),
BlocProvider(
create: (final _) => apiVolumesCubit..load(),
create: (final _) => serverJobsBloc,
),
BlocProvider(create: (final _) => connectionStatusBloc),
BlocProvider(
create: (final _) => apiServerVolumesCubit..load(),
create: (final _) => serverDetailsCubit,
),
BlocProvider(create: (final _) => volumesBloc),
BlocProvider(
create: (final _) => serverJobsCubit..load(),
),
BlocProvider(
create: (final _) => serverDetailsCubit..load(),
),
BlocProvider(
create: (final _) => JobsCubit(
usersCubit: usersCubit,
servicesCubit: servicesCubit,
),
create: (final _) => JobsCubit(),
),
],
child: child,
child: widget.child,
);
}
}

View File

@ -1,13 +1,13 @@
import 'package:get_it/get_it.dart';
import 'package:selfprivacy/logic/get_it/api_config.dart';
import 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
import 'package:selfprivacy/logic/get_it/console.dart';
import 'package:selfprivacy/logic/get_it/navigation.dart';
import 'package:selfprivacy/logic/get_it/timer.dart';
export 'package:selfprivacy/logic/get_it/api_config.dart';
export 'package:selfprivacy/logic/get_it/api_connection_repository.dart';
export 'package:selfprivacy/logic/get_it/console.dart';
export 'package:selfprivacy/logic/get_it/navigation.dart';
export 'package:selfprivacy/logic/get_it/timer.dart';
final GetIt getIt = GetIt.instance;
@ -15,8 +15,11 @@ Future<void> getItSetup() async {
getIt.registerSingleton<NavigationService>(NavigationService());
getIt.registerSingleton<ConsoleModel>(ConsoleModel());
getIt.registerSingleton<TimerModel>(TimerModel());
getIt.registerSingleton<ApiConfigModel>(ApiConfigModel()..init());
getIt.registerSingleton<ApiConnectionRepository>(
ApiConnectionRepository()..init(),
);
await getIt.allReady();
}

View File

@ -20,7 +20,7 @@ class HiveConfig {
Hive.registerAdapter(ServerDomainAdapter());
Hive.registerAdapter(BackupsCredentialAdapter());
Hive.registerAdapter(BackblazeBucketAdapter());
Hive.registerAdapter(ServerVolumeAdapter());
Hive.registerAdapter(ServerProviderVolumeAdapter());
Hive.registerAdapter(UserTypeAdapter());
Hive.registerAdapter(DnsProviderTypeAdapter());
Hive.registerAdapter(ServerProviderTypeAdapter());

View File

@ -150,9 +150,9 @@ type DnsRecord {
recordType: String!
name: String!
content: String!
displayName: String!
ttl: Int!
priority: Int
displayName: String!
}
type GenericBackupConfigReturn implements MutationReturnInterface {
@ -272,6 +272,19 @@ enum RestoreStrategy {
DOWNLOAD_VERIFY_OVERWRITE
}
input SSHSettingsInput {
enable: Boolean!
passwordAuthentication: Boolean!
}
type SSHSettingsMutationReturn implements MutationReturnInterface {
success: Boolean!
message: String!
code: Int!
enable: Boolean!
passwordAuthentication: Boolean!
}
enum ServerProvider {
HETZNER
DIGITALOCEAN
@ -424,9 +437,10 @@ type SystemInfo {
type SystemMutations {
changeTimezone(timezone: String!): TimezoneMutationReturn!
changeAutoUpgradeSettings(settings: AutoUpgradeSettingsInput!): AutoUpgradeSettingsMutationReturn!
runSystemRebuild: GenericMutationReturn!
changeSshSettings(settings: SSHSettingsInput!): SSHSettingsMutationReturn!
runSystemRebuild: GenericJobMutationReturn!
runSystemRollback: GenericMutationReturn!
runSystemUpgrade: GenericMutationReturn!
runSystemUpgrade: GenericJobMutationReturn!
rebootSystem: GenericMutationReturn!
pullRepositoryChanges: GenericMutationReturn!
}

View File

@ -982,6 +982,135 @@ class _CopyWithStubImpl$Input$RecoveryKeyLimitsInput<TRes>
_res;
}
class Input$SSHSettingsInput {
factory Input$SSHSettingsInput({
required bool enable,
required bool passwordAuthentication,
}) =>
Input$SSHSettingsInput._({
r'enable': enable,
r'passwordAuthentication': passwordAuthentication,
});
Input$SSHSettingsInput._(this._$data);
factory Input$SSHSettingsInput.fromJson(Map<String, dynamic> data) {
final result$data = <String, dynamic>{};
final l$enable = data['enable'];
result$data['enable'] = (l$enable as bool);
final l$passwordAuthentication = data['passwordAuthentication'];
result$data['passwordAuthentication'] = (l$passwordAuthentication as bool);
return Input$SSHSettingsInput._(result$data);
}
Map<String, dynamic> _$data;
bool get enable => (_$data['enable'] as bool);
bool get passwordAuthentication => (_$data['passwordAuthentication'] as bool);
Map<String, dynamic> toJson() {
final result$data = <String, dynamic>{};
final l$enable = enable;
result$data['enable'] = l$enable;
final l$passwordAuthentication = passwordAuthentication;
result$data['passwordAuthentication'] = l$passwordAuthentication;
return result$data;
}
CopyWith$Input$SSHSettingsInput<Input$SSHSettingsInput> get copyWith =>
CopyWith$Input$SSHSettingsInput(
this,
(i) => i,
);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (!(other is Input$SSHSettingsInput) ||
runtimeType != other.runtimeType) {
return false;
}
final l$enable = enable;
final lOther$enable = other.enable;
if (l$enable != lOther$enable) {
return false;
}
final l$passwordAuthentication = passwordAuthentication;
final lOther$passwordAuthentication = other.passwordAuthentication;
if (l$passwordAuthentication != lOther$passwordAuthentication) {
return false;
}
return true;
}
@override
int get hashCode {
final l$enable = enable;
final l$passwordAuthentication = passwordAuthentication;
return Object.hashAll([
l$enable,
l$passwordAuthentication,
]);
}
}
abstract class CopyWith$Input$SSHSettingsInput<TRes> {
factory CopyWith$Input$SSHSettingsInput(
Input$SSHSettingsInput instance,
TRes Function(Input$SSHSettingsInput) then,
) = _CopyWithImpl$Input$SSHSettingsInput;
factory CopyWith$Input$SSHSettingsInput.stub(TRes res) =
_CopyWithStubImpl$Input$SSHSettingsInput;
TRes call({
bool? enable,
bool? passwordAuthentication,
});
}
class _CopyWithImpl$Input$SSHSettingsInput<TRes>
implements CopyWith$Input$SSHSettingsInput<TRes> {
_CopyWithImpl$Input$SSHSettingsInput(
this._instance,
this._then,
);
final Input$SSHSettingsInput _instance;
final TRes Function(Input$SSHSettingsInput) _then;
static const _undefined = <dynamic, dynamic>{};
TRes call({
Object? enable = _undefined,
Object? passwordAuthentication = _undefined,
}) =>
_then(Input$SSHSettingsInput._({
..._instance._$data,
if (enable != _undefined && enable != null) 'enable': (enable as bool),
if (passwordAuthentication != _undefined &&
passwordAuthentication != null)
'passwordAuthentication': (passwordAuthentication as bool),
}));
}
class _CopyWithStubImpl$Input$SSHSettingsInput<TRes>
implements CopyWith$Input$SSHSettingsInput<TRes> {
_CopyWithStubImpl$Input$SSHSettingsInput(this._res);
TRes _res;
call({
bool? enable,
bool? passwordAuthentication,
}) =>
_res;
}
class Input$SshMutationInput {
factory Input$SshMutationInput({
required String username,
@ -1928,6 +2057,7 @@ const possibleTypesMap = <String, Set<String>>{
'GenericBackupConfigReturn',
'GenericJobMutationReturn',
'GenericMutationReturn',
'SSHSettingsMutationReturn',
'ServiceJobMutationReturn',
'ServiceMutationReturn',
'TimezoneMutationReturn',

View File

@ -42,6 +42,17 @@ mutation RemoveJob($jobId: String!) {
}
mutation RunSystemRebuild {
system {
runSystemRebuild {
...basicMutationReturnFields
job {
...basicApiJobsFields
}
}
}
}
mutation RunSystemRebuildFallback {
system {
runSystemRebuild {
...basicMutationReturnFields
@ -58,6 +69,17 @@ mutation RunSystemRollback {
}
mutation RunSystemUpgrade {
system {
runSystemUpgrade {
...basicMutationReturnFields
job {
...basicApiJobsFields
}
}
}
}
mutation RunSystemUpgradeFallback {
system {
runSystemUpgrade {
...basicMutationReturnFields

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,11 @@ mixin ServerActionsApi on GraphQLApiMap {
print(response.exception.toString());
}
if (response.parsedData!.system.rebootSystem.success) {
time = DateTime.now().toUtc();
return GenericResult(
data: time,
success: true,
message: response.parsedData!.system.rebootSystem.message,
);
}
} catch (e) {
print(e);
@ -50,23 +54,94 @@ mixin ServerActionsApi on GraphQLApiMap {
}
}
Future<bool> upgrade() async {
Future<GenericResult<ServerJob?>> upgrade() async {
try {
final GraphQLClient client = await getClient();
return _commonBoolRequest(
() async => client.mutate$RunSystemUpgrade(),
);
final result = await client.mutate$RunSystemUpgrade();
if (result.hasException) {
final fallbackResult = await client.mutate$RunSystemUpgradeFallback();
if (fallbackResult.parsedData!.system.runSystemUpgrade.success) {
return GenericResult(
success: true,
data: null,
message: fallbackResult.parsedData!.system.runSystemUpgrade.message,
);
} else {
return GenericResult(
success: false,
message: fallbackResult.parsedData!.system.runSystemUpgrade.message,
data: null,
);
}
} else if (result.parsedData!.system.runSystemUpgrade.success &&
result.parsedData!.system.runSystemUpgrade.job != null) {
return GenericResult(
success: true,
data: ServerJob.fromGraphQL(
result.parsedData!.system.runSystemUpgrade.job!,
),
message: result.parsedData!.system.runSystemUpgrade.message,
);
} else {
return GenericResult(
success: false,
message: result.parsedData!.system.runSystemUpgrade.message,
data: null,
);
}
} catch (e) {
return false;
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
Future<void> apply() async {
Future<GenericResult<ServerJob?>> apply() async {
try {
final GraphQLClient client = await getClient();
await client.mutate$RunSystemRebuild();
final result = await client.mutate$RunSystemRebuild();
if (result.hasException) {
final fallbackResult = await client.mutate$RunSystemRebuildFallback();
if (fallbackResult.parsedData!.system.runSystemRebuild.success) {
return GenericResult(
success: true,
data: null,
message: fallbackResult.parsedData!.system.runSystemRebuild.message,
);
} else {
return GenericResult(
success: false,
message: fallbackResult.parsedData!.system.runSystemRebuild.message,
data: null,
);
}
} else {
if (result.parsedData!.system.runSystemRebuild.success &&
result.parsedData!.system.runSystemRebuild.job != null) {
return GenericResult(
success: true,
data: ServerJob.fromGraphQL(
result.parsedData!.system.runSystemRebuild.job!,
),
message: result.parsedData!.system.runSystemRebuild.message,
);
} else {
return GenericResult(
success: false,
message: result.parsedData!.system.runSystemRebuild.message,
data: null,
);
}
}
} catch (e) {
print(e);
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
}

View File

@ -132,24 +132,55 @@ class ServerApi extends GraphQLApiMap
return usesBinds;
}
Future<void> switchService(final String uid, final bool needTurnOn) async {
Future<GenericResult> switchService(
final String uid,
final bool needTurnOn,
) async {
try {
final GraphQLClient client = await getClient();
if (needTurnOn) {
final variables = Variables$Mutation$EnableService(serviceId: uid);
final mutation = Options$Mutation$EnableService(variables: variables);
await client.mutate$EnableService(mutation);
final result = await client.mutate$EnableService(mutation);
if (result.hasException) {
return GenericResult(
success: false,
message: result.exception.toString(),
data: null,
);
}
return GenericResult(
success: result.parsedData?.services.enableService.success ?? false,
message: result.parsedData?.services.enableService.message,
data: null,
);
} else {
final variables = Variables$Mutation$DisableService(serviceId: uid);
final mutation = Options$Mutation$DisableService(variables: variables);
await client.mutate$DisableService(mutation);
final result = await client.mutate$DisableService(mutation);
if (result.hasException) {
return GenericResult(
success: false,
message: result.exception.toString(),
data: null,
);
}
return GenericResult(
success: result.parsedData?.services.disableService.success ?? false,
message: result.parsedData?.services.disableService.message,
data: null,
);
}
} catch (e) {
print(e);
return GenericResult(
success: false,
message: e.toString(),
data: null,
);
}
}
Future<void> setAutoUpgradeSettings(
Future<GenericResult<AutoUpgradeSettings?>> setAutoUpgradeSettings(
final AutoUpgradeSettings settings,
) async {
try {
@ -164,13 +195,38 @@ class ServerApi extends GraphQLApiMap
final mutation = Options$Mutation$ChangeAutoUpgradeSettings(
variables: variables,
);
await client.mutate$ChangeAutoUpgradeSettings(mutation);
final result = await client.mutate$ChangeAutoUpgradeSettings(mutation);
if (result.hasException) {
return GenericResult<AutoUpgradeSettings?>(
success: false,
message: result.exception.toString(),
data: null,
);
}
return GenericResult<AutoUpgradeSettings?>(
success: result.parsedData?.system.changeAutoUpgradeSettings.success ??
false,
message: result.parsedData?.system.changeAutoUpgradeSettings.message,
data: result.parsedData == null
? null
: AutoUpgradeSettings(
allowReboot: result
.parsedData!.system.changeAutoUpgradeSettings.allowReboot,
enable: result.parsedData!.system.changeAutoUpgradeSettings
.enableAutoUpgrade,
),
);
} catch (e) {
print(e);
return GenericResult<AutoUpgradeSettings?>(
success: false,
message: e.toString(),
data: null,
);
}
}
Future<void> setTimezone(final String timezone) async {
Future<GenericResult<String?>> setTimezone(final String timezone) async {
try {
final GraphQLClient client = await getClient();
final variables = Variables$Mutation$ChangeTimezone(
@ -179,9 +235,26 @@ class ServerApi extends GraphQLApiMap
final mutation = Options$Mutation$ChangeTimezone(
variables: variables,
);
await client.mutate$ChangeTimezone(mutation);
final result = await client.mutate$ChangeTimezone(mutation);
if (result.hasException) {
return GenericResult<String>(
success: false,
message: result.exception.toString(),
data: '',
);
}
return GenericResult<String?>(
success: result.parsedData?.system.changeTimezone.success ?? false,
message: result.parsedData?.system.changeTimezone.message,
data: result.parsedData?.system.changeTimezone.timezone,
);
} catch (e) {
print(e);
return GenericResult<String?>(
success: false,
message: e.toString(),
data: '',
);
}
}

View File

@ -11,6 +11,7 @@ mixin VolumeApi on GraphQLApiMap {
if (response.hasException) {
print(response.exception.toString());
}
// TODO: Rewrite to use fromGraphQL
volumes = response.data!['storage']['volumes']
.map<ServerDiskVolume>((final e) => ServerDiskVolume.fromJson(e))
.toList();

View File

@ -0,0 +1,408 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/initialize_repository_input.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
part 'backups_event.dart';
part 'backups_state.dart';
class BackupsBloc extends Bloc<BackupsEvent, BackupsState> {
BackupsBloc() : super(BackupsInitial()) {
on<BackupsServerLoaded>(
_loadState,
transformer: droppable(),
);
on<BackupsServerReset>(
_resetState,
transformer: droppable(),
);
on<BackupsStateChanged>(
_updateState,
transformer: droppable(),
);
on<InitializeBackupsRepository>(
_initializeRepository,
transformer: droppable(),
);
on<ForceSnapshotListUpdate>(
_forceSnapshotListUpdate,
transformer: droppable(),
);
on<CreateBackups>(
_createBackups,
transformer: sequential(),
);
on<RestoreBackup>(
_restoreBackup,
transformer: sequential(),
);
on<SetAutobackupPeriod>(
_setAutobackupPeriod,
transformer: restartable(),
);
on<SetAutobackupQuotas>(
_setAutobackupQuotas,
transformer: restartable(),
);
on<ForgetSnapshot>(
_forgetSnapshot,
transformer: sequential(),
);
final connectionRepository = getIt<ApiConnectionRepository>();
_apiStatusSubscription = connectionRepository.connectionStatusStream
.listen((final ConnectionStatus connectionStatus) {
switch (connectionStatus) {
case ConnectionStatus.nonexistent:
add(const BackupsServerReset());
isLoaded = false;
break;
case ConnectionStatus.connected:
if (!isLoaded) {
add(const BackupsServerLoaded());
isLoaded = true;
}
break;
default:
break;
}
});
_apiDataSubscription = connectionRepository.dataStream.listen(
(final ApiData apiData) {
if (apiData.backups.data == null || apiData.backupConfig.data == null) {
add(const BackupsServerReset());
isLoaded = false;
} else {
add(
BackupsStateChanged(
apiData.backups.data!,
apiData.backupConfig.data,
),
);
isLoaded = true;
}
},
);
if (connectionRepository.connectionStatus == ConnectionStatus.connected) {
add(const BackupsServerLoaded());
isLoaded = true;
}
}
final BackblazeApi backblaze = BackblazeApi();
Future<void> _loadState(
final BackupsServerLoaded event,
final Emitter<BackupsState> emit,
) async {
BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
final backups = getIt<ApiConnectionRepository>().apiData.backups;
final backupConfig = getIt<ApiConnectionRepository>().apiData.backupConfig;
if (backupConfig.data == null || backups.data == null) {
emit(BackupsLoading());
return;
}
if (bucket != null &&
backupConfig.data!.encryptionKey != bucket.encryptionKey) {
bucket = bucket.copyWith(
encryptionKey: backupConfig.data!.encryptionKey,
);
await getIt<ApiConfigModel>().setBackblazeBucket(bucket);
}
if (backupConfig.data!.isInitialized) {
emit(
BackupsInitialized(
backblazeBucket: bucket,
backupConfig: backupConfig.data,
backups: backups.data ?? [],
),
);
} else {
emit(BackupsUnititialized());
}
}
Future<void> _resetState(
final BackupsServerReset event,
final Emitter<BackupsState> emit,
) async {
emit(BackupsInitial());
}
Future<void> _initializeRepository(
final InitializeBackupsRepository event,
final Emitter<BackupsState> emit,
) async {
if (state is! BackupsUnititialized) {
return;
}
emit(BackupsInitializing());
final String? encryptionKey = getIt<ApiConnectionRepository>()
.apiData
.backupConfig
.data
?.encryptionKey;
if (encryptionKey == null) {
emit(BackupsUnititialized());
getIt<NavigationService>()
.showSnackBar("Couldn't get encryption key from your server.");
return;
}
final BackblazeBucket bucket;
if (state.backblazeBucket == null) {
final String domain = getIt<ApiConnectionRepository>()
.serverDomain!
.domainName
.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
final int serverId = getIt<ApiConnectionRepository>().serverDetails!.id;
String bucketName =
'${DateTime.now().millisecondsSinceEpoch}-$serverId-$domain';
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>().setBackblazeBucket(bucket);
emit(state.copyWith(backblazeBucket: bucket));
} else {
bucket = state.backblazeBucket!;
}
final GenericResult result =
await getIt<ApiConnectionRepository>().api.initializeRepository(
InitializeRepositoryInput(
provider: BackupsProviderType.backblaze,
locationId: bucket.bucketId,
locationName: bucket.bucketName,
login: bucket.applicationKeyId,
password: bucket.applicationKey,
),
);
if (result.success == false) {
getIt<NavigationService>().showSnackBar(
result.message ?? "Couldn't initialize repository on your server.",
);
emit(BackupsUnititialized());
return;
}
getIt<ApiConnectionRepository>().apiData.backupConfig.invalidate();
getIt<ApiConnectionRepository>().apiData.backups.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
getIt<NavigationService>().showSnackBar(
'Backups repository is now initializing. It may take a while.',
);
}
Future<void> _updateState(
final BackupsStateChanged event,
final Emitter<BackupsState> emit,
) async {
if (event.backupConfiguration == null ||
event.backupConfiguration!.isInitialized == false) {
emit(BackupsUnititialized());
return;
}
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
emit(
BackupsInitialized(
backblazeBucket: bucket,
backupConfig: event.backupConfiguration,
backups: event.backups,
),
);
}
Future<void> _forceSnapshotListUpdate(
final ForceSnapshotListUpdate event,
final Emitter<BackupsState> emit,
) async {
final currentState = state;
if (currentState is BackupsInitialized) {
emit(BackupsBusy.fromState(currentState));
getIt<NavigationService>().showSnackBar('backup.refetching_list'.tr());
await getIt<ApiConnectionRepository>().api.forceBackupListReload();
getIt<ApiConnectionRepository>().apiData.backups.invalidate();
emit(currentState);
}
}
Future<void> _createBackups(
final CreateBackups event,
final Emitter<BackupsState> emit,
) async {
final currentState = state;
if (currentState is BackupsInitialized) {
emit(BackupsBusy.fromState(currentState));
for (final service in event.services) {
final GenericResult<ServerJob?> result =
await getIt<ApiConnectionRepository>().api.startBackup(
service.id,
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
}
if (result.data != null) {
getIt<ApiConnectionRepository>()
.apiData
.serverJobs
.data
?.add(result.data!);
}
}
emit(currentState);
getIt<ApiConnectionRepository>().emitData();
}
}
Future<void> _restoreBackup(
final RestoreBackup event,
final Emitter<BackupsState> emit,
) async {
final currentState = state;
if (currentState is BackupsInitialized) {
emit(BackupsBusy.fromState(currentState));
final GenericResult result =
await getIt<ApiConnectionRepository>().api.restoreBackup(
event.backupId,
event.restoreStrategy,
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
}
emit(currentState);
}
}
Future<void> _setAutobackupPeriod(
final SetAutobackupPeriod event,
final Emitter<BackupsState> emit,
) async {
final currentState = state;
if (currentState is BackupsInitialized) {
emit(BackupsBusy.fromState(currentState));
final GenericResult result =
await getIt<ApiConnectionRepository>().api.setAutobackupPeriod(
period: event.period?.inMinutes,
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
}
if (result.success == true) {
getIt<ApiConnectionRepository>().apiData.backupConfig.data =
getIt<ApiConnectionRepository>()
.apiData
.backupConfig
.data
?.copyWith(
autobackupPeriod: event.period,
);
}
emit(currentState);
getIt<ApiConnectionRepository>().emitData();
}
}
Future<void> _setAutobackupQuotas(
final SetAutobackupQuotas event,
final Emitter<BackupsState> emit,
) async {
final currentState = state;
if (currentState is BackupsInitialized) {
emit(BackupsBusy.fromState(currentState));
final GenericResult result =
await getIt<ApiConnectionRepository>().api.setAutobackupQuotas(
event.quotas,
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
}
if (result.success == true) {
getIt<ApiConnectionRepository>().apiData.backupConfig.data =
getIt<ApiConnectionRepository>()
.apiData
.backupConfig
.data
?.copyWith(
autobackupQuotas: event.quotas,
);
}
emit(currentState);
getIt<ApiConnectionRepository>().emitData();
}
}
Future<void> _forgetSnapshot(
final ForgetSnapshot event,
final Emitter<BackupsState> emit,
) async {
final currentState = state;
if (currentState is BackupsInitialized) {
// Optimistically remove the snapshot from the list
getIt<ApiConnectionRepository>().apiData.backups.data =
getIt<ApiConnectionRepository>()
.apiData
.backups
.data
?.where((final Backup backup) => backup.id != event.backupId)
.toList();
emit(BackupsBusy.fromState(currentState));
final GenericResult result =
await getIt<ApiConnectionRepository>().api.forgetSnapshot(
event.backupId,
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
} else if (result.data == false) {
getIt<NavigationService>()
.showSnackBar('backup.forget_snapshot_error'.tr());
}
emit(currentState);
}
}
@override
Future<void> close() {
_apiStatusSubscription.cancel();
_apiDataSubscription.cancel();
return super.close();
}
@override
void onChange(final Change<BackupsState> change) {
super.onChange(change);
}
late StreamSubscription _apiStatusSubscription;
late StreamSubscription _apiDataSubscription;
bool isLoaded = false;
}

View File

@ -0,0 +1,89 @@
part of 'backups_bloc.dart';
sealed class BackupsEvent extends Equatable {
const BackupsEvent();
}
class BackupsServerLoaded extends BackupsEvent {
const BackupsServerLoaded();
@override
List<Object?> get props => [];
}
class BackupsServerReset extends BackupsEvent {
const BackupsServerReset();
@override
List<Object?> get props => [];
}
class InitializeBackupsRepository extends BackupsEvent {
const InitializeBackupsRepository();
@override
List<Object?> get props => [];
}
class BackupsStateChanged extends BackupsEvent {
const BackupsStateChanged(this.backups, this.backupConfiguration);
final List<Backup> backups;
final BackupConfiguration? backupConfiguration;
@override
List<Object?> get props => [backups, backupConfiguration];
}
class ForceSnapshotListUpdate extends BackupsEvent {
const ForceSnapshotListUpdate();
@override
List<Object?> get props => [];
}
class CreateBackups extends BackupsEvent {
const CreateBackups(this.services);
final List<Service> services;
@override
List<Object?> get props => [services];
}
class RestoreBackup extends BackupsEvent {
const RestoreBackup(this.backupId, this.restoreStrategy);
final String backupId;
final BackupRestoreStrategy restoreStrategy;
@override
List<Object?> get props => [backupId, restoreStrategy];
}
class SetAutobackupPeriod extends BackupsEvent {
const SetAutobackupPeriod(this.period);
final Duration? period;
@override
List<Object?> get props => [period];
}
class SetAutobackupQuotas extends BackupsEvent {
const SetAutobackupQuotas(this.quotas);
final AutobackupQuotas quotas;
@override
List<Object?> get props => [quotas];
}
class ForgetSnapshot extends BackupsEvent {
const ForgetSnapshot(this.backupId);
final String backupId;
@override
List<Object?> get props => [backupId];
}

View File

@ -0,0 +1,170 @@
part of 'backups_bloc.dart';
sealed class BackupsState extends Equatable {
BackupsState({
this.backblazeBucket,
});
final apiConnectionRepository = getIt<ApiConnectionRepository>();
final BackblazeBucket? backblazeBucket;
@Deprecated('Infer the initializations status from state')
bool get isInitialized => false;
@Deprecated('Infer the loading status from state')
bool get refreshing => false;
@Deprecated('Infer the prevent actions status from state')
bool get preventActions => true;
List<Backup> get backups => [];
List<Backup> serviceBackups(final String serviceId) => [];
Duration? get autobackupPeriod => null;
AutobackupQuotas? get autobackupQuotas => null;
BackupsState copyWith({required final BackblazeBucket backblazeBucket});
}
class BackupsInitial extends BackupsState {
BackupsInitial({
super.backblazeBucket,
});
@override
List<Object> get props => [];
@override
BackupsInitial copyWith({
final BackblazeBucket? backblazeBucket,
}) =>
BackupsInitial(backblazeBucket: backblazeBucket ?? this.backblazeBucket);
}
class BackupsLoading extends BackupsState {
BackupsLoading({
super.backblazeBucket,
});
@override
List<Object> get props => [];
@override
@Deprecated('Infer the loading status from state')
bool get refreshing => true;
@override
BackupsLoading copyWith({
final BackblazeBucket? backblazeBucket,
}) =>
BackupsLoading(backblazeBucket: backblazeBucket ?? this.backblazeBucket);
}
class BackupsUnititialized extends BackupsState {
BackupsUnititialized({
super.backblazeBucket,
});
@override
List<Object> get props => [];
@override
BackupsUnititialized copyWith({
final BackblazeBucket? backblazeBucket,
}) =>
BackupsUnititialized(
backblazeBucket: backblazeBucket ?? this.backblazeBucket,
);
}
class BackupsInitializing extends BackupsState {
BackupsInitializing({
super.backblazeBucket,
});
@override
List<Object> get props => [];
@override
BackupsInitializing copyWith({
final BackblazeBucket? backblazeBucket,
}) =>
BackupsInitializing(
backblazeBucket: backblazeBucket ?? this.backblazeBucket,
);
}
class BackupsInitialized extends BackupsState {
BackupsInitialized({
final List<Backup> backups = const [],
final BackupConfiguration? backupConfig,
super.backblazeBucket,
}) : _backupsHashCode = Object.hashAll(backups),
_backupConfigHashCode = Object.hashAll([backupConfig]);
final int _backupsHashCode;
final int _backupConfigHashCode;
List<Backup> get _backupList =>
apiConnectionRepository.apiData.backups.data ?? [];
BackupConfiguration? get _backupConfig =>
apiConnectionRepository.apiData.backupConfig.data;
@override
AutobackupQuotas? get autobackupQuotas => _backupConfig?.autobackupQuotas;
@override
Duration? get autobackupPeriod =>
_backupConfig?.autobackupPeriod?.inMinutes == 0
? null
: _backupConfig?.autobackupPeriod;
@override
@Deprecated('Infer the initializations status from state')
bool get isInitialized => true;
@override
@Deprecated('Infer the prevent actions status from state')
bool get preventActions => false;
@override
List<Backup> get backups {
try {
final List<Backup> list = _backupList;
list.sort((final a, final b) => b.time.compareTo(a.time));
return list;
} catch (_) {
return _backupList;
}
}
@override
List<Backup> serviceBackups(final String serviceId) => backups
.where((final backup) => backup.serviceId == serviceId)
.toList(growable: false);
@override
List<Object> get props => [_backupsHashCode, _backupConfigHashCode];
@override
BackupsState copyWith({required final BackblazeBucket backblazeBucket}) =>
BackupsInitialized(
backups: backups,
backupConfig: _backupConfig,
backblazeBucket: backblazeBucket,
);
}
class BackupsBusy extends BackupsInitialized {
BackupsBusy.fromState(final BackupsInitialized state)
: super(
backups: state.backups,
backupConfig: state._backupConfig,
backblazeBucket: state.backblazeBucket,
);
@override
@Deprecated('Infer the prevent actions status from state')
bool get preventActions => true;
@override
List<Object> get props => [];
}

View File

@ -0,0 +1,39 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
part 'connection_status_event.dart';
part 'connection_status_state.dart';
class ConnectionStatusBloc
extends Bloc<ConnectionStatusEvent, ConnectionStatusState> {
ConnectionStatusBloc()
: super(
const ConnectionStatusState(
connectionStatus: ConnectionStatus.nonexistent,
),
) {
on<ConnectionStatusChanged>((final event, final emit) {
emit(ConnectionStatusState(connectionStatus: event.connectionStatus));
});
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiConnectionStatusSubscription =
apiConnectionRepository.connectionStatusStream.listen(
(final ConnectionStatus connectionStatus) {
add(
ConnectionStatusChanged(connectionStatus),
);
},
);
}
StreamSubscription? _apiConnectionStatusSubscription;
@override
Future<void> close() {
_apiConnectionStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,14 @@
part of 'connection_status_bloc.dart';
sealed class ConnectionStatusEvent extends Equatable {
const ConnectionStatusEvent();
}
class ConnectionStatusChanged extends ConnectionStatusEvent {
const ConnectionStatusChanged(this.connectionStatus);
final ConnectionStatus connectionStatus;
@override
List<Object?> get props => [connectionStatus];
}

View File

@ -0,0 +1,12 @@
part of 'connection_status_bloc.dart';
class ConnectionStatusState extends Equatable {
const ConnectionStatusState({
required this.connectionStatus,
});
final ConnectionStatus connectionStatus;
@override
List<Object> get props => [connectionStatus];
}

View File

@ -0,0 +1,110 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
part 'devices_event.dart';
part 'devices_state.dart';
class DevicesBloc extends Bloc<DevicesEvent, DevicesState> {
DevicesBloc() : super(DevicesInitial()) {
on<DevicesListChanged>(
_mapDevicesListChangedToState,
transformer: sequential(),
);
on<DeleteDevice>(
_mapDeleteDeviceToState,
transformer: sequential(),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
add(
DevicesListChanged(apiData.devices.data),
);
},
);
}
StreamSubscription? _apiDataSubscription;
Future<void> _mapDevicesListChangedToState(
final DevicesListChanged event,
final Emitter<DevicesState> emit,
) async {
if (state is DevicesDeleting) {
return;
}
if (event.devices == null) {
emit(DevicesError());
return;
}
emit(DevicesLoaded(devices: event.devices!));
}
Future<void> refresh() async {
getIt<ApiConnectionRepository>().apiData.devices.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
}
Future<void> _mapDeleteDeviceToState(
final DeleteDevice event,
final Emitter<DevicesState> emit,
) async {
// Optimistically remove the device from the list
emit(
DevicesDeleting(
devices: state.devices
.where((final d) => d.name != event.device.name)
.toList(),
),
);
final GenericResult<void> response = await getIt<ApiConnectionRepository>()
.api
.deleteApiToken(event.device.name);
if (response.success) {
getIt<ApiConnectionRepository>().apiData.devices.invalidate();
emit(
DevicesLoaded(
devices: state.devices
.where((final d) => d.name != event.device.name)
.toList(),
),
);
} else {
getIt<NavigationService>()
.showSnackBar(response.message ?? 'Error deleting device');
emit(DevicesLoaded(devices: state.devices));
}
}
Future<String?> getNewDeviceKey() async {
final GenericResult<String> response =
await getIt<ApiConnectionRepository>().api.createDeviceToken();
if (response.success) {
return response.data;
} else {
getIt<NavigationService>().showSnackBar(
response.message ?? 'Error getting new device key',
);
return null;
}
}
@override
void onChange(final Change<DevicesState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,23 @@
part of 'devices_bloc.dart';
sealed class DevicesEvent extends Equatable {
const DevicesEvent();
}
class DevicesListChanged extends DevicesEvent {
const DevicesListChanged(this.devices);
final List<ApiToken>? devices;
@override
List<Object> get props => [];
}
class DeleteDevice extends DevicesEvent {
const DeleteDevice(this.device);
final ApiToken device;
@override
List<Object> get props => [device];
}

View File

@ -0,0 +1,53 @@
part of 'devices_bloc.dart';
sealed class DevicesState extends Equatable {
DevicesState({
required final List<ApiToken> devices,
}) : _hashCode = Object.hashAll(devices);
final int _hashCode;
List<ApiToken> get _devices =>
getIt<ApiConnectionRepository>().apiData.devices.data ?? const [];
List<ApiToken> get devices => _devices;
ApiToken get thisDevice => _devices.firstWhere(
(final device) => device.isCaller,
orElse: () => ApiToken(
name: 'Error fetching device',
isCaller: true,
date: DateTime.now(),
),
);
List<ApiToken> get otherDevices =>
_devices.where((final device) => !device.isCaller).toList();
}
class DevicesInitial extends DevicesState {
DevicesInitial() : super(devices: const []);
@override
List<Object> get props => [_hashCode];
}
class DevicesLoaded extends DevicesState {
DevicesLoaded({required super.devices});
@override
List<Object> get props => [_hashCode];
}
class DevicesError extends DevicesState {
DevicesError() : super(devices: const []);
@override
List<Object> get props => [_hashCode];
}
class DevicesDeleting extends DevicesState {
DevicesDeleting({required super.devices});
@override
List<Object> get props => [_hashCode];
}

View File

@ -0,0 +1,88 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/generic_result.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
part 'recovery_key_event.dart';
part 'recovery_key_state.dart';
class RecoveryKeyBloc extends Bloc<RecoveryKeyEvent, RecoveryKeyState> {
RecoveryKeyBloc() : super(RecoveryKeyInitial()) {
on<RecoveryKeyStatusChanged>(
_mapRecoveryKeyStatusChangedToState,
transformer: sequential(),
);
on<RecoveryKeyStatusRefresh>(
_mapRecoveryKeyStatusRefreshToState,
transformer: droppable(),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
add(
RecoveryKeyStatusChanged(apiData.recoveryKeyStatus.data),
);
},
);
}
StreamSubscription? _apiDataSubscription;
Future<void> _mapRecoveryKeyStatusChangedToState(
final RecoveryKeyStatusChanged event,
final Emitter<RecoveryKeyState> emit,
) async {
if (event.recoveryKeyStatus == null) {
emit(RecoveryKeyError());
return;
}
emit(RecoveryKeyLoaded(keyStatus: event.recoveryKeyStatus));
}
Future<String> generateRecoveryKey({
final DateTime? expirationDate,
final int? numberOfUses,
}) async {
final GenericResult<String> response =
await getIt<ApiConnectionRepository>()
.api
.generateRecoveryToken(expirationDate, numberOfUses);
if (response.success) {
getIt<ApiConnectionRepository>().apiData.recoveryKeyStatus.invalidate();
unawaited(getIt<ApiConnectionRepository>().reload(null));
return response.data;
} else {
throw GenerationError(response.message ?? 'Unknown error');
}
}
Future<void> _mapRecoveryKeyStatusRefreshToState(
final RecoveryKeyEvent event,
final Emitter<RecoveryKeyState> emit,
) async {
emit(RecoveryKeyRefreshing(keyStatus: state._status));
getIt<ApiConnectionRepository>().apiData.recoveryKeyStatus.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
}
@override
void onChange(final Change<RecoveryKeyState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}
class GenerationError extends Error {
GenerationError(this.message);
final String message;
}

View File

@ -0,0 +1,21 @@
part of 'recovery_key_bloc.dart';
sealed class RecoveryKeyEvent extends Equatable {
const RecoveryKeyEvent();
}
class RecoveryKeyStatusChanged extends RecoveryKeyEvent {
const RecoveryKeyStatusChanged(this.recoveryKeyStatus);
final RecoveryKeyStatus? recoveryKeyStatus;
@override
List<Object?> get props => [recoveryKeyStatus];
}
class RecoveryKeyStatusRefresh extends RecoveryKeyEvent {
const RecoveryKeyStatusRefresh();
@override
List<Object?> get props => [];
}

View File

@ -0,0 +1,56 @@
part of 'recovery_key_bloc.dart';
sealed class RecoveryKeyState extends Equatable {
RecoveryKeyState({
required final RecoveryKeyStatus? keyStatus,
}) : _hashCode = keyStatus.hashCode;
final int _hashCode;
RecoveryKeyStatus get _status =>
getIt<ApiConnectionRepository>().apiData.recoveryKeyStatus.data ??
const RecoveryKeyStatus(exists: false, valid: false);
bool get exists => _status.exists;
bool get isValid => _status.valid;
DateTime? get generatedAt => _status.date;
DateTime? get expiresAt => _status.expiration;
int? get usesLeft => _status.usesLeft;
bool get isInvalidBecauseExpired =>
_status.expiration != null &&
_status.expiration!.isBefore(DateTime.now());
bool get isInvalidBecauseUsed =>
_status.usesLeft != null && _status.usesLeft == 0;
}
class RecoveryKeyInitial extends RecoveryKeyState {
RecoveryKeyInitial()
: super(keyStatus: const RecoveryKeyStatus(exists: false, valid: false));
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyRefreshing extends RecoveryKeyState {
RecoveryKeyRefreshing({required super.keyStatus});
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyLoaded extends RecoveryKeyState {
RecoveryKeyLoaded({required super.keyStatus});
@override
List<Object> get props => [_hashCode];
}
class RecoveryKeyError extends RecoveryKeyState {
RecoveryKeyError()
: super(keyStatus: const RecoveryKeyStatus(exists: false, valid: false));
@override
List<Object> get props => [_hashCode];
}

View File

@ -0,0 +1,81 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
export 'package:provider/provider.dart';
part 'server_jobs_state.dart';
part 'server_jobs_event.dart';
class ServerJobsBloc extends Bloc<ServerJobsEvent, ServerJobsState> {
ServerJobsBloc()
: super(
ServerJobsInitialState(),
) {
on<ServerJobsListChanged>(
_mapServerJobsListChangedToState,
transformer: sequential(),
);
on<RemoveServerJob>(
_mapRemoveServerJobToState,
transformer: sequential(),
);
on<RemoveAllFinishedJobs>(
_mapRemoveAllFinishedJobsToState,
transformer: droppable(),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
add(
ServerJobsListChanged([...apiData.serverJobs.data ?? []]),
);
},
);
}
StreamSubscription? _apiDataSubscription;
Future<void> _mapServerJobsListChangedToState(
final ServerJobsListChanged event,
final Emitter<ServerJobsState> emit,
) async {
if (event.serverJobList.isEmpty) {
emit(ServerJobsListEmptyState());
return;
}
final newState =
ServerJobsListWithJobsState(serverJobList: event.serverJobList);
emit(newState);
}
Future<void> _mapRemoveServerJobToState(
final RemoveServerJob event,
final Emitter<ServerJobsState> emit,
) async {
await getIt<ApiConnectionRepository>().removeServerJob(event.uid);
}
Future<void> _mapRemoveAllFinishedJobsToState(
final RemoveAllFinishedJobs event,
final Emitter<ServerJobsState> emit,
) async {
await getIt<ApiConnectionRepository>().removeAllFinishedServerJobs();
}
@override
void onChange(final Change<ServerJobsState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,28 @@
part of 'server_jobs_bloc.dart';
sealed class ServerJobsEvent extends Equatable {
const ServerJobsEvent();
@override
List<Object?> get props => [];
}
class ServerJobsListChanged extends ServerJobsEvent {
const ServerJobsListChanged(this.serverJobList);
final List<ServerJob> serverJobList;
@override
List<Object?> get props => [serverJobList];
}
class RemoveServerJob extends ServerJobsEvent {
const RemoveServerJob(this.uid);
final String uid;
@override
List<Object?> get props => [uid];
}
class RemoveAllFinishedJobs extends ServerJobsEvent {}

View File

@ -0,0 +1,55 @@
part of 'server_jobs_bloc.dart';
sealed class ServerJobsState extends Equatable {
ServerJobsState({
final int? hashCode,
}) : _hashCode = hashCode ?? Object.hashAll([]);
final int? _hashCode;
final apiConnectionRepository = getIt<ApiConnectionRepository>();
List<ServerJob> get _serverJobList =>
apiConnectionRepository.apiData.serverJobs.data ?? [];
List<ServerJob> get serverJobList {
try {
final List<ServerJob> list = _serverJobList;
list.sort((final a, final b) => b.createdAt.compareTo(a.createdAt));
return list;
} on UnsupportedError {
return _serverJobList;
}
}
List<ServerJob> get backupJobList => serverJobList
.where(
// The backup jobs has the format of 'service.<service_id>.backup'
(final job) =>
job.typeId.contains('backup') || job.typeId.contains('restore'),
)
.toList();
bool get hasRemovableJobs => serverJobList.any(
(final job) =>
job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error,
);
@override
List<Object?> get props => [_hashCode];
}
class ServerJobsInitialState extends ServerJobsState {
ServerJobsInitialState() : super(hashCode: Object.hashAll([]));
}
class ServerJobsListEmptyState extends ServerJobsState {
ServerJobsListEmptyState() : super(hashCode: Object.hashAll([]));
}
class ServerJobsListWithJobsState extends ServerJobsState {
ServerJobsListWithJobsState({
required final List<ServerJob> serverJobList,
}) : super(hashCode: Object.hashAll([...serverJobList]));
}

View File

@ -0,0 +1,149 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/service.dart';
part 'services_event.dart';
part 'services_state.dart';
class ServicesBloc extends Bloc<ServicesEvent, ServicesState> {
ServicesBloc() : super(ServicesInitial()) {
on<ServicesListUpdate>(
_updateList,
transformer: sequential(),
);
on<ServicesReload>(
_reload,
transformer: droppable(),
);
on<ServiceRestart>(
_restart,
transformer: sequential(),
);
on<ServiceMove>(
_move,
transformer: sequential(),
);
final connectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = connectionRepository.dataStream.listen(
(final ApiData apiData) {
add(
ServicesListUpdate([...apiData.services.data ?? []]),
);
},
);
if (connectionRepository.connectionStatus == ConnectionStatus.connected) {
add(
ServicesListUpdate(
[...connectionRepository.apiData.services.data ?? []],
),
);
}
}
Future<void> _updateList(
final ServicesListUpdate event,
final Emitter<ServicesState> emit,
) async {
if (event.services.isEmpty) {
emit(ServicesInitial());
return;
}
final newState = ServicesLoaded(
services: event.services,
lockedServices: state._lockedServices,
);
emit(newState);
}
Future<void> _reload(
final ServicesReload event,
final Emitter<ServicesState> emit,
) async {
final currentState = state;
if (currentState is ServicesLoaded) {
emit(ServicesReloading.fromState(currentState));
getIt<ApiConnectionRepository>().apiData.services.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
}
}
Future<void> awaitReload() async {
final currentState = state;
if (currentState is ServicesLoaded) {
getIt<ApiConnectionRepository>().apiData.services.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
}
}
Future<void> _restart(
final ServiceRestart event,
final Emitter<ServicesState> emit,
) async {
emit(
state.copyWith(
lockedServices: [
...state._lockedServices,
ServiceLock(
serviceId: event.service.id,
lockDuration: const Duration(seconds: 15),
),
],
),
);
final result = await getIt<ApiConnectionRepository>()
.api
.restartService(event.service.id);
if (!result.success) {
getIt<NavigationService>().showSnackBar('jobs.generic_error'.tr());
return;
}
if (!result.data) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
return;
}
}
Future<void> _move(
final ServiceMove event,
final Emitter<ServicesState> emit,
) async {
final migrationJob = await getIt<ApiConnectionRepository>()
.api
.moveService(event.service.id, event.destination);
if (!migrationJob.success) {
getIt<NavigationService>()
.showSnackBar(migrationJob.message ?? 'jobs.generic_error'.tr());
}
if (migrationJob.data != null) {
getIt<ApiConnectionRepository>()
.apiData
.serverJobs
.data
?.add(migrationJob.data!);
getIt<ApiConnectionRepository>().emitData();
}
}
late StreamSubscription _apiDataSubscription;
@override
void onChange(final Change<ServicesState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription.cancel();
return super.close();
}
}

View File

@ -0,0 +1,40 @@
part of 'services_bloc.dart';
sealed class ServicesEvent extends Equatable {
const ServicesEvent();
}
class ServicesListUpdate extends ServicesEvent {
const ServicesListUpdate(this.services);
final List<Service> services;
@override
List<Object?> get props => [services];
}
class ServicesReload extends ServicesEvent {
const ServicesReload();
@override
List<Object?> get props => [];
}
class ServiceRestart extends ServicesEvent {
const ServiceRestart(this.service);
final Service service;
@override
List<Object?> get props => [service];
}
class ServiceMove extends ServicesEvent {
const ServiceMove(this.service, this.destination);
final Service service;
final String destination;
@override
List<Object?> get props => [service, destination];
}

View File

@ -0,0 +1,115 @@
part of 'services_bloc.dart';
sealed class ServicesState extends Equatable {
ServicesState({final List<ServiceLock> lockedServices = const []})
: _lockedServices =
lockedServices.where((final lock) => lock.isLocked).toList();
final List<ServiceLock> _lockedServices;
List<Service> get services;
List<String> get lockedServices => _lockedServices
.where((final lock) => lock.isLocked)
.map((final lock) => lock.serviceId)
.toList();
List<Service> get servicesThatCanBeBackedUp => services
.where(
(final service) => service.canBeBackedUp,
)
.toList();
bool isServiceLocked(final String serviceId) =>
lockedServices.contains(serviceId);
Service? getServiceById(final String id) {
final service = services.firstWhere(
(final service) => service.id == id,
orElse: () => Service.empty,
);
if (service.id == 'empty') {
return null;
}
return service;
}
ServicesState copyWith({
final List<Service>? services,
final List<ServiceLock>? lockedServices,
});
}
class ServiceLock extends Equatable {
ServiceLock({
required this.serviceId,
required this.lockDuration,
}) : lockTime = DateTime.now();
final String serviceId;
final Duration lockDuration;
final DateTime lockTime;
bool get isLocked => DateTime.now().isBefore(lockTime.add(lockDuration));
@override
List<Object?> get props => [serviceId, lockDuration, lockTime];
}
class ServicesInitial extends ServicesState {
@override
List<Object> get props => [];
@override
List<Service> get services => [];
@override
ServicesState copyWith({
final List<Service>? services,
final List<ServiceLock>? lockedServices,
}) =>
ServicesInitial();
}
class ServicesLoaded extends ServicesState {
ServicesLoaded({
required final List<Service> services,
required super.lockedServices,
}) : _servicesHachCode = Object.hashAll([...services]);
final int _servicesHachCode;
final apiConnectionRepository = getIt<ApiConnectionRepository>();
List<Service> get _services =>
apiConnectionRepository.apiData.services.data ?? [];
@override
List<Service> get services => _services;
@override
List<Object?> get props => [_servicesHachCode, _lockedServices];
@override
ServicesLoaded copyWith({
final List<Service>? services,
final List<ServiceLock>? lockedServices,
}) =>
ServicesLoaded(
services: services ?? this.services,
lockedServices: lockedServices ?? _lockedServices,
);
}
class ServicesReloading extends ServicesLoaded {
ServicesReloading({
required super.services,
required super.lockedServices,
});
ServicesReloading.fromState(final ServicesLoaded state)
: super(
services: state.services,
lockedServices: state._lockedServices,
);
@override
List<Object?> get props => [services, lockedServices];
}

View File

@ -0,0 +1,105 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
part 'users_event.dart';
part 'users_state.dart';
class UsersBloc extends Bloc<UsersEvent, UsersState> {
UsersBloc() : super(UsersInitial()) {
on<UsersListChanged>(
_updateList,
transformer: sequential(),
);
on<UsersListRefresh>(
_reload,
transformer: droppable(),
);
on<UsersConnectionStatusChanged>(
_mapConnectionStatusChangedToState,
transformer: sequential(),
);
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiConnectionStatusSubscription =
apiConnectionRepository.connectionStatusStream.listen(
(final ConnectionStatus connectionStatus) {
add(
UsersConnectionStatusChanged(connectionStatus),
);
},
);
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
add(
UsersListChanged(apiData.users.data ?? []),
);
},
);
}
Future<void> _updateList(
final UsersListChanged event,
final Emitter<UsersState> emit,
) async {
if (event.users.isEmpty) {
emit(UsersInitial());
return;
}
final newState = UsersLoaded(
users: event.users,
);
emit(newState);
}
Future<void> refresh() async {
getIt<ApiConnectionRepository>().apiData.users.invalidate();
await getIt<ApiConnectionRepository>().reload(null);
}
Future<void> _reload(
final UsersListRefresh event,
final Emitter<UsersState> emit,
) async {
emit(UsersRefreshing(users: state.users));
await refresh();
}
Future<void> _mapConnectionStatusChangedToState(
final UsersConnectionStatusChanged event,
final Emitter<UsersState> emit,
) async {
switch (event.connectionStatus) {
case ConnectionStatus.nonexistent:
emit(UsersInitial());
break;
case ConnectionStatus.connected:
if (state is! UsersLoaded) {
emit(UsersRefreshing(users: state.users));
}
case ConnectionStatus.reconnecting:
case ConnectionStatus.offline:
case ConnectionStatus.unauthorized:
break;
}
}
StreamSubscription? _apiDataSubscription;
StreamSubscription? _apiConnectionStatusSubscription;
@override
void onChange(final Change<UsersState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
_apiConnectionStatusSubscription?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,30 @@
part of 'users_bloc.dart';
sealed class UsersEvent extends Equatable {
const UsersEvent();
}
class UsersListChanged extends UsersEvent {
const UsersListChanged(this.users);
final List<User> users;
@override
List<Object> get props => [users];
}
class UsersListRefresh extends UsersEvent {
const UsersListRefresh();
@override
List<Object> get props => [];
}
class UsersConnectionStatusChanged extends UsersEvent {
const UsersConnectionStatusChanged(this.connectionStatus);
final ConnectionStatus connectionStatus;
@override
List<Object> get props => [connectionStatus];
}

View File

@ -1,10 +1,14 @@
part of 'users_cubit.dart';
part of 'users_bloc.dart';
class UsersState extends ServerInstallationDependendState {
const UsersState(this.users, this.isLoading);
sealed class UsersState extends Equatable {
UsersState({
required final List<User> users,
}) : _hashCode = Object.hashAll(users);
final List<User> users;
final bool isLoading;
final int _hashCode;
List<User> get users =>
getIt<ApiConnectionRepository>().apiData.users.data ?? const [];
User get rootUser =>
users.firstWhere((final user) => user.type == UserType.root);
@ -15,9 +19,6 @@ class UsersState extends ServerInstallationDependendState {
List<User> get normalUsers =>
users.where((final user) => user.type == UserType.normal).toList();
@override
List<Object> get props => [users, isLoading];
/// Makes a copy of existing users list, but places 'primary'
/// to the beginning and sorts the rest alphabetically
///
@ -44,17 +45,29 @@ class UsersState extends ServerInstallationDependendState {
return primaryUser == null ? normalUsers : [primaryUser] + normalUsers;
}
UsersState copyWith({
final List<User>? users,
final bool? isLoading,
}) =>
UsersState(
users ?? this.users,
isLoading ?? this.isLoading,
);
bool isLoginRegistered(final String login) =>
users.any((final User user) => user.login == login);
bool get isEmpty => users.isEmpty;
}
class UsersInitial extends UsersState {
UsersInitial() : super(users: const []);
@override
List<Object> get props => [_hashCode];
}
class UsersRefreshing extends UsersState {
UsersRefreshing({required super.users});
@override
List<Object> get props => [_hashCode];
}
class UsersLoaded extends UsersState {
UsersLoaded({required super.users});
@override
List<Object> get props => [_hashCode];
}

View File

@ -0,0 +1,246 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/json/server_disk_volume.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
part 'volumes_event.dart';
part 'volumes_state.dart';
class VolumesBloc extends Bloc<VolumesEvent, VolumesState> {
VolumesBloc() : super(VolumesInitial()) {
on<VolumesServerLoaded>(
_loadState,
transformer: droppable(),
);
on<VolumesServerReset>(
_resetState,
transformer: droppable(),
);
on<VolumesServerStateChanged>(
_updateState,
transformer: droppable(),
);
on<VolumeResize>(
_resizeVolume,
transformer: droppable(),
);
final connectionRepository = getIt<ApiConnectionRepository>();
_apiStatusSubscription = connectionRepository.connectionStatusStream
.listen((final ConnectionStatus connectionStatus) {
switch (connectionStatus) {
case ConnectionStatus.nonexistent:
add(const VolumesServerReset());
isLoaded = false;
break;
case ConnectionStatus.connected:
if (!isLoaded) {
add(const VolumesServerLoaded());
isLoaded = true;
}
break;
default:
break;
}
});
_apiDataSubscription = connectionRepository.dataStream.listen(
(final ApiData apiData) {
if (apiData.volumes.data == null) {
add(const VolumesServerReset());
} else {
add(
VolumesServerStateChanged(
apiData.volumes.data!,
),
);
}
},
);
}
late StreamSubscription _apiStatusSubscription;
late StreamSubscription _apiDataSubscription;
bool isLoaded = false;
Future<Price?> getPricePerGb() async {
if (ProvidersController.currentServerProvider == null) {
return null;
}
Price? price;
final pricingResult =
await ProvidersController.currentServerProvider!.getAdditionalPricing();
if (pricingResult.data == null || !pricingResult.success) {
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
return price;
}
price = pricingResult.data!.perVolumeGb;
return price;
}
Future<void> _loadState(
final VolumesServerLoaded event,
final Emitter<VolumesState> emit,
) async {
if (ProvidersController.currentServerProvider == null) {
return;
}
emit(VolumesLoading());
final volumesResult =
await ProvidersController.currentServerProvider!.getVolumes();
if (!volumesResult.success || volumesResult.data.isEmpty) {
emit(VolumesInitial());
return;
}
final serverVolumes = getIt<ApiConnectionRepository>().apiData.volumes.data;
if (serverVolumes == null) {
emit(VolumesLoading(providerVolumes: volumesResult.data));
return;
} else {
emit(
VolumesLoaded(
diskStatus: DiskStatus.fromVolumes(
serverVolumes,
volumesResult.data,
),
providerVolumes: volumesResult.data,
serverVolumesHashCode: Object.hashAll(serverVolumes),
),
);
}
}
Future<void> _resetState(
final VolumesServerReset event,
final Emitter<VolumesState> emit,
) async {
emit(VolumesInitial());
}
@override
void onChange(final Change<VolumesState> change) {
super.onChange(change);
}
@override
Future<void> close() async {
await _apiStatusSubscription.cancel();
await _apiDataSubscription.cancel();
await super.close();
}
Future<void> invalidateCache() async {
getIt<ApiConnectionRepository>().apiData.volumes.invalidate();
}
Future<void> _updateState(
final VolumesServerStateChanged event,
final Emitter<VolumesState> emit,
) async {
final serverVolumes = event.volumes;
final providerVolumes = state.providerVolumes;
if (state is VolumesLoading) {
emit(
VolumesLoaded(
diskStatus: DiskStatus.fromVolumes(
serverVolumes,
providerVolumes,
),
providerVolumes: providerVolumes,
serverVolumesHashCode: Object.hashAll(serverVolumes),
),
);
return;
}
emit(
state.copyWith(
diskStatus: DiskStatus.fromVolumes(
serverVolumes,
providerVolumes,
),
providerVolumes: providerVolumes,
serverVolumesHashCode: Object.hashAll(serverVolumes),
),
);
}
Future<void> _resizeVolume(
final VolumeResize event,
final Emitter<VolumesState> emit,
) async {
if (state is! VolumesLoaded) {
return;
}
getIt<NavigationService>().showSnackBar(
'storage.extending_volume_started'.tr(),
);
emit(
VolumesResizing(
serverVolumesHashCode: state._serverVolumesHashCode,
diskStatus: state.diskStatus,
providerVolumes: state.providerVolumes,
),
);
final resizedResult =
await ProvidersController.currentServerProvider!.resizeVolume(
event.volume.providerVolume!,
event.newSize,
);
if (!resizedResult.success || !resizedResult.data) {
getIt<NavigationService>().showSnackBar(
'storage.extending_volume_error'.tr(),
);
emit(
VolumesLoaded(
serverVolumesHashCode: state._serverVolumesHashCode,
diskStatus: state.diskStatus,
providerVolumes: state.providerVolumes,
),
);
return;
}
getIt<NavigationService>().showSnackBar(
'storage.extending_volume_waiting'.tr(),
);
await Future.delayed(const Duration(seconds: 10));
await getIt<ApiConnectionRepository>().api.resizeVolume(event.volume.name);
getIt<NavigationService>().showSnackBar(
'storage.extending_volume_server_waiting'.tr(),
);
await Future.delayed(const Duration(seconds: 20));
getIt<NavigationService>().showSnackBar(
'storage.extending_volume_rebooting'.tr(),
);
emit(
VolumesLoaded(
serverVolumesHashCode: state._serverVolumesHashCode,
diskStatus: state.diskStatus,
providerVolumes: state.providerVolumes,
),
);
await getIt<ApiConnectionRepository>().api.reboot();
}
}

View File

@ -0,0 +1,43 @@
part of 'volumes_bloc.dart';
sealed class VolumesEvent extends Equatable {
const VolumesEvent();
}
class VolumesServerLoaded extends VolumesEvent {
const VolumesServerLoaded();
@override
List<Object> get props => [];
}
class VolumesServerReset extends VolumesEvent {
const VolumesServerReset();
@override
List<Object> get props => [];
}
class VolumesServerStateChanged extends VolumesEvent {
const VolumesServerStateChanged(
this.volumes,
);
final List<ServerDiskVolume> volumes;
@override
List<Object> get props => [volumes];
}
class VolumeResize extends VolumesEvent {
const VolumeResize(
this.volume,
this.newSize,
);
final DiskVolume volume;
final DiskSize newSize;
@override
List<Object> get props => [volume, newSize];
}

View File

@ -0,0 +1,122 @@
part of 'volumes_bloc.dart';
sealed class VolumesState extends Equatable {
const VolumesState({
required this.diskStatus,
required final serverVolumesHashCode,
this.providerVolumes = const [],
}) : _serverVolumesHashCode = serverVolumesHashCode;
final DiskStatus diskStatus;
final List<ServerProviderVolume> providerVolumes;
List<DiskVolume> get volumes => diskStatus.diskVolumes;
final int? _serverVolumesHashCode;
DiskVolume getVolume(final String volumeName) => volumes.firstWhere(
(final volume) => volume.name == volumeName,
orElse: () => DiskVolume(),
);
bool get isProviderVolumesLoaded => providerVolumes.isNotEmpty;
VolumesState copyWith({
required final int? serverVolumesHashCode,
final DiskStatus? diskStatus,
final List<ServerProviderVolume>? providerVolumes,
});
}
class VolumesInitial extends VolumesState {
VolumesInitial()
: super(
diskStatus: DiskStatus(),
serverVolumesHashCode: null,
);
@override
List<Object?> get props => [providerVolumes, _serverVolumesHashCode];
@override
VolumesInitial copyWith({
required final int? serverVolumesHashCode,
final DiskStatus? diskStatus,
final List<ServerProviderVolume>? providerVolumes,
}) =>
VolumesInitial();
}
class VolumesLoading extends VolumesState {
VolumesLoading({
super.serverVolumesHashCode,
final DiskStatus? diskStatus,
final List<ServerProviderVolume>? providerVolumes,
}) : super(
diskStatus: diskStatus ?? DiskStatus(),
providerVolumes: providerVolumes ?? const [],
);
@override
List<Object?> get props => [providerVolumes, _serverVolumesHashCode];
@override
VolumesLoading copyWith({
required final int? serverVolumesHashCode,
final DiskStatus? diskStatus,
final List<ServerProviderVolume>? providerVolumes,
}) =>
VolumesLoading(
diskStatus: diskStatus ?? this.diskStatus,
providerVolumes: providerVolumes ?? this.providerVolumes,
serverVolumesHashCode: serverVolumesHashCode ?? _serverVolumesHashCode!,
);
}
class VolumesLoaded extends VolumesState {
const VolumesLoaded({
required super.serverVolumesHashCode,
required super.diskStatus,
final List<ServerProviderVolume>? providerVolumes,
}) : super(
providerVolumes: providerVolumes ?? const [],
);
@override
List<Object?> get props => [providerVolumes, _serverVolumesHashCode];
@override
VolumesLoaded copyWith({
final DiskStatus? diskStatus,
final List<ServerProviderVolume>? providerVolumes,
final int? serverVolumesHashCode,
}) =>
VolumesLoaded(
diskStatus: diskStatus ?? this.diskStatus,
providerVolumes: providerVolumes ?? this.providerVolumes,
serverVolumesHashCode: serverVolumesHashCode ?? _serverVolumesHashCode!,
);
}
class VolumesResizing extends VolumesState {
const VolumesResizing({
required super.serverVolumesHashCode,
required super.diskStatus,
final List<ServerProviderVolume>? providerVolumes,
}) : super(
providerVolumes: providerVolumes ?? const [],
);
@override
List<Object?> get props => [providerVolumes, _serverVolumesHashCode];
@override
VolumesResizing copyWith({
final DiskStatus? diskStatus,
final List<ServerProviderVolume>? providerVolumes,
final int? serverVolumesHashCode,
}) =>
VolumesResizing(
diskStatus: diskStatus ?? this.diskStatus,
providerVolumes: providerVolumes ?? this.providerVolumes,
serverVolumesHashCode: serverVolumesHashCode ?? _serverVolumesHashCode!,
);
}

View File

@ -1,41 +0,0 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
export 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
part 'authentication_dependend_state.dart';
abstract class ServerInstallationDependendCubit<
T extends ServerInstallationDependendState> extends Cubit<T> {
ServerInstallationDependendCubit(
this.serverInstallationCubit,
final T initState,
) : super(initState) {
authCubitSubscription =
serverInstallationCubit.stream.listen(checkAuthStatus);
checkAuthStatus(serverInstallationCubit.state);
}
void checkAuthStatus(final ServerInstallationState state) {
if (state is ServerInstallationFinished) {
load();
} else if (state is ServerInstallationEmpty) {
clear();
}
}
late StreamSubscription authCubitSubscription;
final ServerInstallationCubit serverInstallationCubit;
void load();
void clear();
@override
Future<void> close() {
authCubitSubscription.cancel();
return super.close();
}
}

View File

@ -1,279 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.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';
class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
BackupsCubit(final ServerInstallationCubit serverInstallationCubit)
: super(
serverInstallationCubit,
const BackupsState(preventActions: true),
);
final ServerApi api = ServerApi();
final BackblazeApi backblaze = BackblazeApi();
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
final BackupConfiguration? backupConfig =
await api.getBackupsConfiguration();
final List<Backup> backups = await api.getBackups();
backups.sort((final a, final b) => b.time.compareTo(a.time));
emit(
state.copyWith(
backblazeBucket: bucket,
isInitialized: backupConfig?.isInitialized,
autobackupPeriod: backupConfig?.autobackupPeriod ?? Duration.zero,
autobackupQuotas: backupConfig?.autobackupQuotas,
backups: backups,
preventActions: false,
refreshing: false,
),
);
}
}
Future<void> initializeBackups() async {
emit(state.copyWith(preventActions: true));
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 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 =
'${DateTime.now().millisecondsSinceEpoch}-$serverId-$domain';
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,
),
);
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(preventActions: false));
}
Future<void> reuploadKey() async {
emit(state.copyWith(preventActions: true));
BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
if (bucket == null) {
emit(state.copyWith(isInitialized: false));
} else {
String login = bucket.applicationKeyId;
String password = bucket.applicationKey;
if (login.isEmpty || password.isEmpty) {
final BackblazeApplicationKey key =
await backblaze.createKey(bucket.bucketId);
login = key.applicationKeyId;
password = key.applicationKey;
bucket = BackblazeBucket(
bucketId: bucket.bucketId,
bucketName: bucket.bucketName,
encryptionKey: bucket.encryptionKey,
applicationKey: password,
applicationKeyId: login,
);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
emit(state.copyWith(backblazeBucket: bucket));
}
final GenericResult result = await api.initializeRepository(
InitializeRepositoryInput(
provider: BackupsProviderType.backblaze,
locationId: bucket.bucketId,
locationName: bucket.bucketName,
login: login,
password: password,
),
);
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'.tr());
await updateBackups();
}
}
}
@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 backups = await api.getBackups();
backups.sort((final a, final b) => b.time.compareTo(a.time));
final backupConfig = await api.getBackupsConfiguration();
emit(
state.copyWith(
backups: backups,
refreshTimer: refreshTimeFromState(),
refreshing: false,
isInitialized: backupConfig?.isInitialized ?? false,
autobackupPeriod: backupConfig?.autobackupPeriod,
autobackupQuotas: backupConfig?.autobackupQuotas,
),
);
if (useTimer) {
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
}
}
Future<void> forceUpdateBackups() async {
emit(state.copyWith(preventActions: true));
getIt<NavigationService>().showSnackBar('backup.refetching_list'.tr());
await api.forceBackupListReload();
emit(state.copyWith(preventActions: false));
}
Future<void> createMultipleBackups(final List<Service> services) async {
emit(state.copyWith(preventActions: true));
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));
}
Future<void> restoreBackup(
final String backupId,
final BackupRestoreStrategy strategy,
) async {
emit(state.copyWith(preventActions: true));
await api.restoreBackup(backupId, strategy);
emit(state.copyWith(preventActions: false));
}
Future<void> setAutobackupPeriod(final Duration? period) async {
emit(state.copyWith(preventActions: true));
final result = await api.setAutobackupPeriod(period: period?.inMinutes);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
} else {
getIt<NavigationService>()
.showSnackBar('backup.autobackup_period_set'.tr());
emit(
state.copyWith(
preventActions: false,
autobackupPeriod: period ?? Duration.zero,
),
);
}
await updateBackups();
}
Future<void> setAutobackupQuotas(final AutobackupQuotas quotas) async {
emit(state.copyWith(preventActions: true));
final result = await api.setAutobackupQuotas(quotas);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
} else {
getIt<NavigationService>().showSnackBar('backup.quotas_set'.tr());
emit(
state.copyWith(
preventActions: false,
autobackupQuotas: quotas,
),
);
}
await updateBackups();
}
Future<void> forgetSnapshot(final String snapshotId) async {
final result = await api.forgetSnapshot(snapshotId);
if (!result.success) {
getIt<NavigationService>().showSnackBar('jobs.generic_error'.tr());
return;
}
if (result.data == false) {
getIt<NavigationService>()
.showSnackBar('backup.forget_snapshot_error'.tr());
}
// Optimistic update
final backups = state.backups;
final index =
backups.indexWhere((final snapshot) => snapshot.id == snapshotId);
if (index != -1) {
backups.removeAt(index);
emit(state.copyWith(backups: backups));
}
await updateBackups();
}
@override
void clear() async {
emit(const BackupsState());
}
}

View File

@ -1,61 +0,0 @@
part of 'backups_cubit.dart';
class BackupsState extends ServerInstallationDependendState {
const BackupsState({
this.isInitialized = false,
this.backups = const [],
this.preventActions = true,
this.refreshTimer = const Duration(seconds: 60),
this.refreshing = true,
this.autobackupPeriod,
this.backblazeBucket,
this.autobackupQuotas,
});
final bool isInitialized;
final List<Backup> backups;
final bool preventActions;
final Duration refreshTimer;
final bool refreshing;
final Duration? autobackupPeriod;
final BackblazeBucket? backblazeBucket;
final AutobackupQuotas? autobackupQuotas;
List<Backup> serviceBackups(final String serviceId) => backups
.where((final backup) => backup.serviceId == serviceId)
.toList(growable: false);
@override
List<Object> get props => [
isInitialized,
backups,
preventActions,
refreshTimer,
refreshing,
];
BackupsState copyWith({
final bool? isInitialized,
final List<Backup>? backups,
final bool? preventActions,
final Duration? refreshTimer,
final bool? refreshing,
final Duration? autobackupPeriod,
final BackblazeBucket? backblazeBucket,
final AutobackupQuotas? autobackupQuotas,
}) =>
BackupsState(
isInitialized: isInitialized ?? this.isInitialized,
backups: backups ?? this.backups,
preventActions: preventActions ?? this.preventActions,
refreshTimer: refreshTimer ?? this.refreshTimer,
refreshing: refreshing ?? this.refreshing,
// The autobackupPeriod might be null, so if the duration is set to 0, we
// set it to null.
autobackupPeriod: autobackupPeriod?.inSeconds == 0
? null
: autobackupPeriod ?? this.autobackupPeriod,
backblazeBucket: backblazeBucket ?? this.backblazeBucket,
autobackupQuotas: autobackupQuotas ?? this.autobackupQuotas,
);
}

View File

@ -1,36 +1,52 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
export 'package:provider/provider.dart';
part 'client_jobs_state.dart';
class JobsCubit extends Cubit<JobsState> {
JobsCubit({
required this.usersCubit,
required this.servicesCubit,
}) : super(JobsStateEmpty());
JobsCubit() : super(JobsStateEmpty()) {
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
if (apiData.serverJobs.data != null &&
apiData.serverJobs.data!.isNotEmpty) {
_handleServerJobs(apiData.serverJobs.data!);
}
},
);
}
final ServerApi api = ServerApi();
final UsersCubit usersCubit;
final ServicesCubit servicesCubit;
StreamSubscription? _apiDataSubscription;
void addJob(final ClientJob job) {
final jobs = currentJobList;
if (job.canAddTo(jobs)) {
_updateJobsState([
...jobs,
...[job],
]);
void _handleServerJobs(final List<ServerJob> jobs) {
if (state is! JobsStateLoading) {
return;
}
if (state.rebuildJobUid == null) {
return;
}
// Find a job with the uid of the rebuild job
final ServerJob? rebuildJob = jobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null ||
rebuildJob.status == JobStatusEnum.error ||
rebuildJob.status == JobStatusEnum.finished) {
emit((state as JobsStateLoading).finished());
}
}
void addJob(final ClientJob job) async {
emit(state.addJob(job));
}
void removeJob(final String id) {
@ -38,61 +54,145 @@ class JobsCubit extends Cubit<JobsState> {
emit(newState);
}
List<ClientJob> get currentJobList {
final List<ClientJob> jobs = <ClientJob>[];
if (state is JobsStateWithJobs) {
jobs.addAll((state as JobsStateWithJobs).clientJobList);
}
return jobs;
}
void _updateJobsState(final List<ClientJob> newJobs) {
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
emit(JobsStateWithJobs(newJobs));
}
Future<void> rebootServer() async {
emit(JobsStateLoading());
final rebootResult = await api.reboot();
if (rebootResult.success && rebootResult.data != null) {
getIt<NavigationService>().showSnackBar('jobs.reboot_success'.tr());
} else {
getIt<NavigationService>().showSnackBar('jobs.reboot_failed'.tr());
if (state is JobsStateEmpty) {
emit(
JobsStateLoading(
[RebootServerJob(status: JobStatusEnum.running)],
null,
const [],
),
);
final rebootResult = await getIt<ApiConnectionRepository>().api.reboot();
if (rebootResult.success && rebootResult.data != null) {
emit(
JobsStateFinished(
[
RebootServerJob(
status: JobStatusEnum.finished,
message: rebootResult.message,
),
],
null,
const [],
),
);
} else {
emit(
JobsStateFinished(
[RebootServerJob(status: JobStatusEnum.error)],
null,
const [],
),
);
}
}
emit(JobsStateEmpty());
}
Future<void> upgradeServer() async {
emit(JobsStateLoading());
final bool isPullSuccessful = await api.pullConfigurationUpdate();
final bool isSuccessful = await api.upgrade();
if (isSuccessful) {
if (!isPullSuccessful) {
getIt<NavigationService>().showSnackBar('jobs.config_pull_failed'.tr());
if (state is JobsStateEmpty) {
emit(
JobsStateLoading(
[UpgradeServerJob(status: JobStatusEnum.running)],
null,
const [],
),
);
final result = await getIt<ApiConnectionRepository>().api.upgrade();
if (result.success && result.data != null) {
emit(
JobsStateLoading(
[UpgradeServerJob(status: JobStatusEnum.finished)],
result.data!.uid,
const [],
),
);
} else {
getIt<NavigationService>().showSnackBar('jobs.upgrade_success'.tr());
emit(
JobsStateFinished(
[UpgradeServerJob(status: JobStatusEnum.error)],
null,
const [],
),
);
}
} else {
getIt<NavigationService>().showSnackBar('jobs.upgrade_failed'.tr());
}
emit(JobsStateEmpty());
}
Future<void> applyAll() async {
if (state is JobsStateWithJobs) {
final List<ClientJob> jobs = (state as JobsStateWithJobs).clientJobList;
emit(JobsStateLoading());
emit(JobsStateLoading(jobs, null, const []));
await Future<void>.delayed(Duration.zero);
final rebuildRequired = jobs.any((final job) => job.requiresRebuild);
for (final ClientJob job in jobs) {
job.execute(this);
emit(
(state as JobsStateLoading)
.updateJobStatus(job.id, JobStatusEnum.running),
);
final (result, message) = await job.execute();
if (result) {
emit(
(state as JobsStateLoading).updateJobStatus(
job.id,
JobStatusEnum.finished,
message: message,
),
);
} else {
emit(
(state as JobsStateLoading)
.updateJobStatus(job.id, JobStatusEnum.error, message: message),
);
}
}
await api.pullConfigurationUpdate();
await api.apply();
await servicesCubit.load();
emit(JobsStateEmpty());
if (!rebuildRequired) {
emit((state as JobsStateLoading).finished());
return;
}
final rebuildResult = await getIt<ApiConnectionRepository>().api.apply();
if (rebuildResult.success) {
if (rebuildResult.data != null) {
emit(
(state as JobsStateLoading)
.copyWith(rebuildJobUid: rebuildResult.data!.uid),
);
} else {
emit((state as JobsStateLoading).finished());
}
} else {
emit((state as JobsStateLoading).finished());
}
}
}
Future<void> acknowledgeFinished() async {
if (state is! JobsStateFinished) {
return;
}
final rebuildJobUid = state.rebuildJobUid;
if ((state as JobsStateFinished).postponedJobs.isNotEmpty) {
emit(JobsStateWithJobs((state as JobsStateFinished).postponedJobs));
} else {
emit(JobsStateEmpty());
}
if (rebuildJobUid != null) {
await getIt<ApiConnectionRepository>().removeServerJob(rebuildJobUid);
}
}
@override
void onChange(final Change<JobsState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View File

@ -1,17 +1,32 @@
part of 'client_jobs_cubit.dart';
abstract class JobsState extends Equatable {
sealed class JobsState extends Equatable {
String? get rebuildJobUid => null;
JobsState addJob(final ClientJob job);
@override
List<Object?> get props => [];
}
class JobsStateLoading extends JobsState {}
class JobsStateEmpty extends JobsState {
@override
JobsStateWithJobs addJob(final ClientJob job) {
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs([job]);
}
class JobsStateEmpty extends JobsState {}
@override
List<Object?> get props => [];
}
class JobsStateWithJobs extends JobsState {
JobsStateWithJobs(this.clientJobList);
final List<ClientJob> clientJobList;
bool get rebuildRequired =>
clientJobList.any((final job) => job.requiresRebuild);
JobsState removeById(final String id) {
final List<ClientJob> newJobsList =
clientJobList.where((final element) => element.id != id).toList();
@ -22,5 +37,135 @@ class JobsStateWithJobs extends JobsState {
}
@override
List<Object?> get props => clientJobList;
List<Object?> get props => [clientJobList];
@override
JobsState addJob(final ClientJob job) {
if (job is ReplaceableJob) {
final List<ClientJob> newJobsList = clientJobList
.where((final element) => element.runtimeType != job.runtimeType)
.toList();
if (job.shouldRemoveInsteadOfAdd(clientJobList)) {
getIt<NavigationService>().showSnackBar('jobs.job_removed'.tr());
} else {
newJobsList.add(job);
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
}
if (newJobsList.isEmpty) {
return JobsStateEmpty();
}
return JobsStateWithJobs(newJobsList);
}
if (job.canAddTo(clientJobList)) {
final List<ClientJob> newJobsList = [...clientJobList, job];
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs(newJobsList);
}
return this;
}
}
class JobsStateLoading extends JobsState {
JobsStateLoading(this.clientJobList, this.rebuildJobUid, this.postponedJobs);
final List<ClientJob> clientJobList;
@override
final String? rebuildJobUid;
bool get rebuildRequired =>
clientJobList.any((final job) => job.requiresRebuild);
final List<ClientJob> postponedJobs;
JobsStateLoading updateJobStatus(
final String id,
final JobStatusEnum status, {
final String? message,
}) {
final List<ClientJob> newJobsList = clientJobList.map((final job) {
if (job.id == id) {
return job.copyWithNewStatus(status: status, message: message);
}
return job;
}).toList();
return JobsStateLoading(newJobsList, rebuildJobUid, postponedJobs);
}
JobsStateLoading copyWith({
final List<ClientJob>? clientJobList,
final String? rebuildJobUid,
final List<ClientJob>? postponedJobs,
}) =>
JobsStateLoading(
clientJobList ?? this.clientJobList,
rebuildJobUid ?? this.rebuildJobUid,
postponedJobs ?? this.postponedJobs,
);
JobsStateFinished finished() =>
JobsStateFinished(clientJobList, rebuildJobUid, postponedJobs);
@override
List<Object?> get props => [clientJobList, rebuildJobUid, postponedJobs];
@override
JobsState addJob(final ClientJob job) {
if (job is ReplaceableJob) {
final List<ClientJob> newPostponedJobs = postponedJobs
.where((final element) => element.runtimeType != job.runtimeType)
.toList();
if (job.shouldRemoveInsteadOfAdd(postponedJobs)) {
getIt<NavigationService>().showSnackBar('jobs.job_removed'.tr());
} else {
newPostponedJobs.add(job);
getIt<NavigationService>().showSnackBar('jobs.job_postponed'.tr());
}
return JobsStateLoading(clientJobList, rebuildJobUid, newPostponedJobs);
}
if (job.canAddTo(postponedJobs)) {
final List<ClientJob> newPostponedJobs = [...postponedJobs, job];
getIt<NavigationService>().showSnackBar('jobs.job_postponed'.tr());
return JobsStateLoading(clientJobList, rebuildJobUid, newPostponedJobs);
}
return this;
}
}
class JobsStateFinished extends JobsState {
JobsStateFinished(this.clientJobList, this.rebuildJobUid, this.postponedJobs);
final List<ClientJob> clientJobList;
@override
final String? rebuildJobUid;
bool get rebuildRequired =>
clientJobList.any((final job) => job.requiresRebuild);
final List<ClientJob> postponedJobs;
@override
List<Object?> get props => [clientJobList, rebuildJobUid, postponedJobs];
@override
JobsState addJob(final ClientJob job) {
if (job is ReplaceableJob) {
final List<ClientJob> newPostponedJobs = postponedJobs
.where((final element) => element.runtimeType != job.runtimeType)
.toList();
if (job.shouldRemoveInsteadOfAdd(postponedJobs)) {
getIt<NavigationService>().showSnackBar('jobs.job_removed'.tr());
} else {
newPostponedJobs.add(job);
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
}
if (newPostponedJobs.isEmpty) {
return JobsStateEmpty();
}
return JobsStateWithJobs(newPostponedJobs);
}
if (job.canAddTo(postponedJobs)) {
final List<ClientJob> newPostponedJobs = [...postponedJobs, job];
getIt<NavigationService>().showSnackBar('jobs.job_added'.tr());
return JobsStateWithJobs(newPostponedJobs);
}
return this;
}
}

View File

@ -1,77 +0,0 @@
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
part 'devices_state.dart';
class ApiDevicesCubit
extends ServerInstallationDependendCubit<ApiDevicesState> {
ApiDevicesCubit(final ServerInstallationCubit serverInstallationCubit)
: super(serverInstallationCubit, const ApiDevicesState.initial());
final ServerApi api = ServerApi();
@override
void load() async {
// if (serverInstallationCubit.state is ServerInstallationFinished) {
_refetch();
// }
}
Future<void> refresh() async {
emit(ApiDevicesState([state.thisDevice], LoadingStatus.refreshing));
_refetch();
}
void _refetch() async {
final List<ApiToken>? devices = await _getApiTokens();
if (devices != null) {
emit(ApiDevicesState(devices, LoadingStatus.success));
} else {
emit(const ApiDevicesState([], LoadingStatus.error));
}
}
Future<List<ApiToken>?> _getApiTokens() async {
final GenericResult<List<ApiToken>> response = await api.getApiTokens();
if (response.success) {
return response.data;
} else {
return null;
}
}
Future<void> deleteDevice(final ApiToken device) async {
final GenericResult<void> response = await api.deleteApiToken(device.name);
if (response.success) {
emit(
ApiDevicesState(
state.devices.where((final d) => d.name != device.name).toList(),
LoadingStatus.success,
),
);
} else {
getIt<NavigationService>()
.showSnackBar(response.message ?? 'Error deleting device');
}
}
Future<String?> getNewDeviceKey() async {
final GenericResult<String> response = await api.createDeviceToken();
if (response.success) {
return response.data;
} else {
getIt<NavigationService>().showSnackBar(
response.message ?? 'Error getting new device key',
);
return null;
}
}
@override
void clear() {
emit(const ApiDevicesState.initial());
}
}

View File

@ -1,34 +0,0 @@
part of 'devices_cubit.dart';
class ApiDevicesState extends ServerInstallationDependendState {
const ApiDevicesState(this._devices, this.status);
const ApiDevicesState.initial() : this(const [], LoadingStatus.uninitialized);
final List<ApiToken> _devices;
final LoadingStatus status;
List<ApiToken> get devices => _devices;
ApiToken get thisDevice => _devices.firstWhere(
(final device) => device.isCaller,
orElse: () => ApiToken(
name: 'Error fetching device',
isCaller: true,
date: DateTime.now(),
),
);
List<ApiToken> get otherDevices =>
_devices.where((final device) => !device.isCaller).toList();
ApiDevicesState copyWith({
final List<ApiToken>? devices,
final LoadingStatus? status,
}) =>
ApiDevicesState(
devices ?? _devices,
status ?? this.status,
);
@override
List<Object?> get props => [_devices];
}

View File

@ -1,9 +1,8 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_connection_dependent/server_connection_dependent_cubit.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
@ -11,11 +10,9 @@ import 'package:selfprivacy/utils/network_utils.dart';
part 'dns_records_state.dart';
class DnsRecordsCubit
extends ServerInstallationDependendCubit<DnsRecordsState> {
DnsRecordsCubit(final ServerInstallationCubit serverInstallationCubit)
class DnsRecordsCubit extends ServerConnectionDependentCubit<DnsRecordsState> {
DnsRecordsCubit()
: super(
serverInstallationCubit,
const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing),
);
@ -30,39 +27,36 @@ class DnsRecordsCubit
),
);
if (serverInstallationCubit.state is ServerInstallationFinished) {
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final String? ipAddress =
serverInstallationCubit.state.serverDetails?.ip4;
final ServerDomain? domain = getIt<ApiConnectionRepository>().serverDomain;
final String? ipAddress =
getIt<ApiConnectionRepository>().serverDetails?.ip4;
if (domain == null || ipAddress == null) {
emit(const DnsRecordsState());
return;
}
final List<DnsRecord> allDnsRecords = await api.getDnsRecords();
allDnsRecords.removeWhere((final record) => record.type == 'AAAA');
final foundRecords = await validateDnsRecords(
domain,
extractDkimRecord(allDnsRecords)?.content ?? '',
allDnsRecords,
ipAddress,
);
if (!foundRecords.success || foundRecords.data.isEmpty) {
emit(const DnsRecordsState(dnsState: DnsRecordsStatus.error));
return;
}
emit(
DnsRecordsState(
dnsRecords: foundRecords.data,
dnsState: foundRecords.data.any((final r) => r.isSatisfied == false)
? DnsRecordsStatus.error
: DnsRecordsStatus.good,
),
);
if (domain == null || ipAddress == null) {
emit(const DnsRecordsState());
return;
}
final List<DnsRecord> allDnsRecords = await api.getDnsRecords();
allDnsRecords.removeWhere((final record) => record.type == 'AAAA');
final foundRecords = await validateDnsRecords(
domain,
extractDkimRecord(allDnsRecords)?.content ?? '',
allDnsRecords,
);
if (!foundRecords.success || foundRecords.data.isEmpty) {
emit(const DnsRecordsState());
return;
}
emit(
DnsRecordsState(
dnsRecords: foundRecords.data,
dnsState: foundRecords.data.any((final r) => r.isSatisfied == false)
? DnsRecordsStatus.error
: DnsRecordsStatus.good,
),
);
}
/// Tries to check whether all known DNS records on the domain by ip4
@ -74,19 +68,7 @@ class DnsRecordsCubit
final ServerDomain domain,
final String dkimPublicKey,
final List<DnsRecord> pendingDnsRecords,
final String ip4,
) async {
final matchMap = await validateDnsMatch(domain.domainName, ['api'], ip4);
if (matchMap.values.any((final status) => status != DnsRecordStatus.ok)) {
getIt<NavigationService>().showSnackBar(
'domain.domain_validation_failure'.tr(),
);
return GenericResult(
success: false,
data: [],
);
}
final result = await ProvidersController.currentDnsProvider!
.getDnsRecords(domain: domain);
if (result.data.isEmpty || !result.success) {
@ -185,7 +167,7 @@ class DnsRecordsCubit
final List<DnsRecord> records = await api.getDnsRecords();
/// TODO: Error handling?
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final ServerDomain? domain = getIt<ApiConnectionRepository>().serverDomain;
await ProvidersController.currentDnsProvider!.removeDomainRecords(
records: records,
domain: domain!,

View File

@ -1,8 +1,8 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
class FieldCubitFactory {
FieldCubitFactory(this.context);
@ -27,7 +27,7 @@ class FieldCubitFactory {
),
ValidationModel(
(final String login) =>
context.read<UsersCubit>().state.isLoginRegistered(login),
context.read<UsersBloc>().state.isLoginRegistered(login),
'validations.already_exist'.tr(),
),
RequiredStringValidation('validations.required'.tr()),

View File

@ -1,150 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/price.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
part 'provider_volume_state.dart';
class ApiProviderVolumeCubit
extends ServerInstallationDependendCubit<ApiProviderVolumeState> {
ApiProviderVolumeCubit(final ServerInstallationCubit serverInstallationCubit)
: super(serverInstallationCubit, const ApiProviderVolumeState.initial());
final ServerApi serverApi = ServerApi();
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
unawaited(_refetch());
}
}
Future<Price?> getPricePerGb() async {
Price? price;
final pricingResult =
await ProvidersController.currentServerProvider!.getAdditionalPricing();
if (pricingResult.data == null || !pricingResult.success) {
getIt<NavigationService>().showSnackBar('server.pricing_error'.tr());
return price;
}
price = pricingResult.data!.perVolumeGb;
return price;
}
Future<void> refresh() async {
emit(const ApiProviderVolumeState([], LoadingStatus.refreshing, false));
unawaited(_refetch());
}
Future<void> _refetch() async {
if (ProvidersController.currentServerProvider == null) {
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
}
final volumesResult =
await ProvidersController.currentServerProvider!.getVolumes();
if (!volumesResult.success || volumesResult.data.isEmpty) {
return emit(const ApiProviderVolumeState([], LoadingStatus.error, false));
}
emit(
ApiProviderVolumeState(
volumesResult.data,
LoadingStatus.success,
false,
),
);
}
Future<void> attachVolume(final DiskVolume volume) async {
final ServerHostingDetails server = getIt<ApiConfigModel>().serverDetails!;
await ProvidersController.currentServerProvider!
.attachVolume(volume.providerVolume!, server.id);
unawaited(refresh());
}
Future<void> detachVolume(final DiskVolume volume) async {
await ProvidersController.currentServerProvider!
.detachVolume(volume.providerVolume!);
unawaited(refresh());
}
Future<bool> resizeVolume(
final DiskVolume volume,
final DiskSize newSize,
final Function() callback,
) async {
getIt<NavigationService>().showSnackBar(
'Starting resize',
);
emit(state.copyWith(isResizing: true));
final resizedResult =
await ProvidersController.currentServerProvider!.resizeVolume(
volume.providerVolume!,
newSize,
);
if (!resizedResult.success || !resizedResult.data) {
getIt<NavigationService>().showSnackBar(
'storage.extending_volume_error'.tr(),
);
emit(state.copyWith(isResizing: false));
return false;
}
getIt<NavigationService>().showSnackBar(
'Provider volume resized, waiting 10 seconds',
);
await Future.delayed(const Duration(seconds: 10));
await ServerApi().resizeVolume(volume.name);
getIt<NavigationService>().showSnackBar(
'Server volume resized, waiting 20 seconds',
);
await Future.delayed(const Duration(seconds: 20));
getIt<NavigationService>().showSnackBar(
'Restarting server',
);
await refresh();
emit(state.copyWith(isResizing: false));
await callback();
await serverApi.reboot();
return true;
}
Future<void> createVolume(final DiskSize size) async {
final ServerVolume? volume = (await ProvidersController
.currentServerProvider!
.createVolume(size.gibibyte.toInt()))
.data;
final diskVolume = DiskVolume(providerVolume: volume);
await attachVolume(diskVolume);
await Future.delayed(const Duration(seconds: 10));
await ServerApi().mountVolume(volume!.name);
unawaited(refresh());
}
Future<void> deleteVolume(final DiskVolume volume) async {
await ProvidersController.currentServerProvider!
.deleteVolume(volume.providerVolume!);
unawaited(refresh());
}
@override
void clear() {
emit(const ApiProviderVolumeState.initial());
}
}

View File

@ -1,27 +0,0 @@
part of 'provider_volume_cubit.dart';
class ApiProviderVolumeState extends ServerInstallationDependendState {
const ApiProviderVolumeState(this._volumes, this.status, this.isResizing);
const ApiProviderVolumeState.initial()
: this(const [], LoadingStatus.uninitialized, false);
final List<ServerVolume> _volumes;
final LoadingStatus status;
final bool isResizing;
List<ServerVolume> get volumes => _volumes;
ApiProviderVolumeState copyWith({
final List<ServerVolume>? volumes,
final LoadingStatus? status,
final bool? isResizing,
}) =>
ApiProviderVolumeState(
volumes ?? _volumes,
status ?? this.status,
isResizing ?? this.isResizing,
);
@override
List<Object?> get props => [_volumes, status, isResizing];
}

View File

@ -1,19 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/logic/models/provider.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
export 'package:provider/provider.dart';
export 'package:selfprivacy/logic/models/state_types.dart';
part 'providers_state.dart';
class ProvidersCubit extends Cubit<ProvidersState> {
ProvidersCubit() : super(InitialProviderState());
void connect(final ProviderModel provider) {
final ProvidersState newState =
state.updateElement(provider, StateType.stable);
emit(newState);
}
}

View File

@ -1,44 +0,0 @@
part of 'providers_cubit.dart';
class ProvidersState extends Equatable {
const ProvidersState(this.all);
final List<ProviderModel> all;
ProvidersState updateElement(
final ProviderModel provider,
final StateType newState,
) {
final List<ProviderModel> newList = [...all];
final int index = newList.indexOf(provider);
newList[index] = provider.updateState(newState);
return ProvidersState(newList);
}
List<ProviderModel> get connected => all
.where((final service) => service.state != StateType.uninitialized)
.toList();
List<ProviderModel> get uninitialized => all
.where((final service) => service.state == StateType.uninitialized)
.toList();
bool get isFullyInitialized => uninitialized.isEmpty;
@override
List<Object> get props => all;
}
class InitialProviderState extends ProvidersState {
InitialProviderState()
: super(
ProviderType.values
.map(
(final type) => ProviderModel(
state: StateType.uninitialized,
type: type,
),
)
.toList(),
);
}

View File

@ -1,81 +0,0 @@
import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
part 'recovery_key_state.dart';
class RecoveryKeyCubit
extends ServerInstallationDependendCubit<RecoveryKeyState> {
RecoveryKeyCubit(final ServerInstallationCubit serverInstallationCubit)
: super(serverInstallationCubit, const RecoveryKeyState.initial());
final ServerApi api = ServerApi();
@override
void load() async {
// if (serverInstallationCubit.state is ServerInstallationFinished) {
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
if (status == null) {
emit(state.copyWith(loadingStatus: LoadingStatus.error));
} else {
emit(
state.copyWith(
status: status,
loadingStatus: LoadingStatus.success,
),
);
}
// } else {
// emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
// }
}
Future<RecoveryKeyStatus?> _getRecoveryKeyStatus() async {
final GenericResult<RecoveryKeyStatus?> response =
await api.getRecoveryTokenStatus();
if (response.success) {
return response.data;
} else {
return null;
}
}
Future<void> refresh() async {
emit(state.copyWith(loadingStatus: LoadingStatus.refreshing));
final RecoveryKeyStatus? status = await _getRecoveryKeyStatus();
if (status == null) {
emit(state.copyWith(loadingStatus: LoadingStatus.error));
} else {
emit(
state.copyWith(status: status, loadingStatus: LoadingStatus.success),
);
}
}
Future<String> generateRecoveryKey({
final DateTime? expirationDate,
final int? numberOfUses,
}) async {
final GenericResult<String> response =
await api.generateRecoveryToken(expirationDate, numberOfUses);
if (response.success) {
unawaited(refresh());
return response.data;
} else {
throw GenerationError(response.message ?? 'Unknown error');
}
}
@override
void clear() {
emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized));
}
}
class GenerationError extends Error {
GenerationError(this.message);
final String message;
}

View File

@ -1,39 +0,0 @@
part of 'recovery_key_cubit.dart';
class RecoveryKeyState extends ServerInstallationDependendState {
const RecoveryKeyState(this._status, this.loadingStatus);
const RecoveryKeyState.initial()
: this(
const RecoveryKeyStatus(exists: false, valid: false),
LoadingStatus.refreshing,
);
final RecoveryKeyStatus _status;
final LoadingStatus loadingStatus;
bool get exists => _status.exists;
bool get isValid => _status.valid;
DateTime? get generatedAt => _status.date;
DateTime? get expiresAt => _status.expiration;
int? get usesLeft => _status.usesLeft;
bool get isInvalidBecauseExpired =>
_status.expiration != null &&
_status.expiration!.isBefore(DateTime.now());
bool get isInvalidBecauseUsed =>
_status.usesLeft != null && _status.usesLeft == 0;
@override
List<Object> get props => [_status, loadingStatus];
RecoveryKeyState copyWith({
final RecoveryKeyStatus? status,
final LoadingStatus? loadingStatus,
}) =>
RecoveryKeyState(
status ?? _status,
loadingStatus ?? this.loadingStatus,
);
}

View File

@ -0,0 +1,51 @@
import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
export 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
part 'server_connection_dependent_state.dart';
abstract class ServerConnectionDependentCubit<
T extends ServerInstallationDependendState> extends Cubit<T> {
ServerConnectionDependentCubit(
super.initState,
) {
final connectionRepository = getIt<ApiConnectionRepository>();
apiStatusSubscription =
connectionRepository.connectionStatusStream.listen(checkAuthStatus);
checkAuthStatus(connectionRepository.connectionStatus);
}
void checkAuthStatus(final ConnectionStatus state) {
switch (state) {
case ConnectionStatus.nonexistent:
clear();
isLoaded = false;
break;
case ConnectionStatus.connected:
if (!isLoaded) {
load();
isLoaded = true;
}
break;
default:
break;
}
}
late StreamSubscription apiStatusSubscription;
bool isLoaded = false;
void load();
void clear();
@override
Future<void> close() {
apiStatusSubscription.cancel();
return super.close();
}
}

View File

@ -1,4 +1,4 @@
part of 'authentication_dependend_cubit.dart';
part of 'server_connection_dependent_cubit.dart';
abstract class ServerInstallationDependendState extends Equatable {
const ServerInstallationDependendState();

View File

@ -1,35 +1,75 @@
import 'dart:async';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart';
import 'package:selfprivacy/logic/cubit/server_connection_dependent/server_connection_dependent_cubit.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/system_settings.dart';
import 'package:selfprivacy/logic/models/timezone_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
part 'server_detailed_info_state.dart';
class ServerDetailsCubit
extends ServerInstallationDependendCubit<ServerDetailsState> {
ServerDetailsCubit(final ServerInstallationCubit serverInstallationCubit)
: super(serverInstallationCubit, ServerDetailsInitial());
extends ServerConnectionDependentCubit<ServerDetailsState> {
ServerDetailsCubit() : super(const ServerDetailsInitial()) {
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
if (apiData.settings.data != null) {
_handleServerSettings(apiData.settings.data!);
}
},
);
}
ServerDetailsRepository repository = ServerDetailsRepository();
StreamSubscription? _apiDataSubscription;
void _handleServerSettings(final SystemSettings settings) {
emit(
Loaded(
metadata: state.metadata,
serverTimezone: TimeZoneSettings.fromString(settings.timezone),
autoUpgradeSettings: settings.autoUpgradeSettings,
),
);
}
Future<List<ServerMetadataEntity>> get _metadata async {
List<ServerMetadataEntity> data = [];
final serverProviderApi = ProvidersController.currentServerProvider;
final dnsProviderApi = ProvidersController.currentDnsProvider;
if (serverProviderApi != null && dnsProviderApi != null) {
final serverId = getIt<ApiConfigModel>().serverDetails?.id ?? 0;
final metadataResult = await serverProviderApi.getMetadata(serverId);
metadataResult.data.add(
ServerMetadataEntity(
trId: 'server.dns_provider',
value: dnsProviderApi.type.displayName,
type: MetadataType.other,
),
);
data = metadataResult.data;
}
return data;
}
void check() async {
final bool isReadyToCheck = getIt<ApiConfigModel>().serverDetails != null;
try {
if (isReadyToCheck) {
emit(ServerDetailsLoading());
final ServerDetailsRepositoryDto data = await repository.load();
emit(const ServerDetailsLoading());
final List<ServerMetadataEntity> metadata = await _metadata;
emit(
Loaded(
metadata: data.metadata,
autoUpgradeSettings: data.autoUpgradeSettings,
serverTimezone: data.serverTimezone,
checkTime: DateTime.now(),
state.copyWith(
metadata: metadata,
),
);
} else {
emit(ServerDetailsNotReady());
emit(const ServerDetailsNotReady());
}
} on StateError {
print('Tried to emit server info state when cubit is closed');
@ -38,11 +78,17 @@ class ServerDetailsCubit
@override
void clear() {
emit(ServerDetailsNotReady());
emit(const ServerDetailsNotReady());
}
@override
void load() async {
check();
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View File

@ -1,68 +0,0 @@
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/server_metadata.dart';
import 'package:selfprivacy/logic/models/timezone_settings.dart';
import 'package:selfprivacy/logic/providers/providers_controller.dart';
class ServerDetailsRepository {
ServerApi server = ServerApi();
Future<ServerDetailsRepositoryDto> load() async {
final settings = await server.getSystemSettings();
return ServerDetailsRepositoryDto(
autoUpgradeSettings: settings.autoUpgradeSettings,
metadata: await metadata,
serverTimezone: TimeZoneSettings.fromString(
settings.timezone,
),
);
}
Future<List<ServerMetadataEntity>> get metadata async {
List<ServerMetadataEntity> data = [];
final serverProviderApi = ProvidersController.currentServerProvider;
final dnsProviderApi = ProvidersController.currentDnsProvider;
if (serverProviderApi != null && dnsProviderApi != null) {
final serverId = getIt<ApiConfigModel>().serverDetails?.id ?? 0;
final metadataResult = await serverProviderApi.getMetadata(serverId);
metadataResult.data.add(
ServerMetadataEntity(
trId: 'server.dns_provider',
value: dnsProviderApi.type.displayName,
type: MetadataType.other,
),
);
data = metadataResult.data;
}
return data;
}
Future<void> setAutoUpgradeSettings(
final AutoUpgradeSettings settings,
) async {
await server.setAutoUpgradeSettings(settings);
}
Future<void> setTimezone(
final String timezone,
) async {
if (timezone.isNotEmpty) {
await server.setTimezone(timezone);
}
}
}
class ServerDetailsRepositoryDto {
ServerDetailsRepositoryDto({
required this.metadata,
required this.serverTimezone,
required this.autoUpgradeSettings,
});
final List<ServerMetadataEntity> metadata;
final TimeZoneSettings serverTimezone;
final AutoUpgradeSettings autoUpgradeSettings;
}

View File

@ -1,37 +1,78 @@
part of 'server_detailed_info_cubit.dart';
abstract class ServerDetailsState extends ServerInstallationDependendState {
const ServerDetailsState();
const ServerDetailsState({
required this.metadata,
});
final List<ServerMetadataEntity> metadata;
@override
List<Object> get props => [];
List<Object> get props => [metadata];
ServerDetailsState copyWith({
final List<ServerMetadataEntity>? metadata,
});
}
class ServerDetailsInitial extends ServerDetailsState {}
class ServerDetailsInitial extends ServerDetailsState {
const ServerDetailsInitial({super.metadata = const []});
class ServerDetailsLoading extends ServerDetailsState {}
@override
ServerDetailsInitial copyWith({final List<ServerMetadataEntity>? metadata}) =>
ServerDetailsInitial(
metadata: metadata ?? this.metadata,
);
}
class ServerDetailsNotReady extends ServerDetailsState {}
class ServerDetailsLoading extends ServerDetailsState {
const ServerDetailsLoading({super.metadata = const []});
class Loading extends ServerDetailsState {}
@override
ServerDetailsLoading copyWith({final List<ServerMetadataEntity>? metadata}) =>
ServerDetailsLoading(
metadata: metadata ?? this.metadata,
);
}
class ServerDetailsNotReady extends ServerDetailsState {
const ServerDetailsNotReady({super.metadata = const []});
@override
ServerDetailsNotReady copyWith({
final List<ServerMetadataEntity>? metadata,
}) =>
ServerDetailsNotReady(
metadata: metadata ?? this.metadata,
);
}
class Loaded extends ServerDetailsState {
const Loaded({
required this.metadata,
required super.metadata,
required this.serverTimezone,
required this.autoUpgradeSettings,
required this.checkTime,
});
final List<ServerMetadataEntity> metadata;
final TimeZoneSettings serverTimezone;
final AutoUpgradeSettings autoUpgradeSettings;
final DateTime checkTime;
@override
List<Object> get props => [
metadata,
serverTimezone,
autoUpgradeSettings,
checkTime,
];
@override
Loaded copyWith({
final List<ServerMetadataEntity>? metadata,
final TimeZoneSettings? serverTimezone,
final AutoUpgradeSettings? autoUpgradeSettings,
final DateTime? checkTime,
}) =>
Loaded(
metadata: metadata ?? this.metadata,
serverTimezone: serverTimezone ?? this.serverTimezone,
autoUpgradeSettings: autoUpgradeSettings ?? this.autoUpgradeSettings,
);
}

View File

@ -233,7 +233,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
try {
bucket = await BackblazeApi()
.fetchBucket(backblazeCredential, configuration);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket!);
await getIt<ApiConfigModel>().setBackblazeBucket(bucket!);
} catch (e) {
print(e);
}
@ -484,6 +484,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
if (dkimCreated) {
await repository.saveHasFinalChecked(true);
emit(dataState.finish());
getIt<ApiConnectionRepository>().init();
} else {
runDelayed(
finishCheckIfServerIsOkay,
@ -724,7 +725,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
ip4: server.ip,
id: server.id,
createTime: server.created,
volume: ServerVolume(
volume: ServerProviderVolume(
id: 0,
name: 'recovered_volume',
sizeByte: 0,
@ -802,6 +803,7 @@ class ServerInstallationCubit extends Cubit<ServerInstallationState> {
serverTypeIdentificator: serverType.data!.identifier,
);
emit(updatedState.finish());
getIt<ApiConnectionRepository>().init();
}
@override

View File

@ -313,7 +313,7 @@ class ServerInstallationRepository {
if (result.success) {
return ServerHostingDetails(
apiToken: result.data,
volume: ServerVolume(
volume: ServerProviderVolume(
id: 0,
name: '',
sizeByte: 0,
@ -350,7 +350,7 @@ class ServerInstallationRepository {
if (result.success) {
return ServerHostingDetails(
apiToken: result.data,
volume: ServerVolume(
volume: ServerProviderVolume(
id: 0,
name: '',
sizeByte: 0,
@ -385,7 +385,7 @@ class ServerInstallationRepository {
if (await serverApi.isHttpServerWorking()) {
return ServerHostingDetails(
apiToken: apiToken,
volume: ServerVolume(
volume: ServerProviderVolume(
id: 0,
name: '',
serverId: 0,
@ -416,7 +416,7 @@ class ServerInstallationRepository {
if (result.success) {
return ServerHostingDetails(
apiToken: result.data,
volume: ServerVolume(
volume: ServerProviderVolume(
id: 0,
name: '',
sizeByte: 0,
@ -470,7 +470,7 @@ class ServerInstallationRepository {
Future<void> saveServerDetails(
final ServerHostingDetails serverDetails,
) async {
await getIt<ApiConfigModel>().storeServerDetails(serverDetails);
await getIt<ApiConfigModel>().setServerDetails(serverDetails);
}
Future<void> deleteServerDetails() async {
@ -483,18 +483,18 @@ class ServerInstallationRepository {
}
Future<void> saveDnsProviderType(final DnsProviderType type) async {
await getIt<ApiConfigModel>().storeDnsProviderType(type);
await getIt<ApiConfigModel>().setDnsProviderType(type);
}
Future<void> saveServerProviderKey(final String key) async {
await getIt<ApiConfigModel>().storeServerProviderKey(key);
await getIt<ApiConfigModel>().setServerProviderKey(key);
}
Future<void> saveServerType(final ServerType serverType) async {
await getIt<ApiConfigModel>().storeServerTypeIdentifier(
await getIt<ApiConfigModel>().setServerTypeIdentifier(
serverType.identifier,
);
await getIt<ApiConfigModel>().storeServerLocation(
await getIt<ApiConfigModel>().setServerLocation(
serverType.location.identifier,
);
}
@ -507,7 +507,7 @@ class ServerInstallationRepository {
Future<void> saveBackblazeKey(
final BackupsCredential backblazeCredential,
) async {
await getIt<ApiConfigModel>().storeBackblazeCredential(backblazeCredential);
await getIt<ApiConfigModel>().setBackblazeCredential(backblazeCredential);
}
Future<void> deleteBackblazeKey() async {
@ -516,7 +516,7 @@ class ServerInstallationRepository {
}
Future<void> setDnsApiToken(final String key) async {
await getIt<ApiConfigModel>().storeDnsProviderKey(key);
await getIt<ApiConfigModel>().setDnsProviderKey(key);
}
Future<void> deleteDnsProviderKey() async {
@ -525,7 +525,7 @@ class ServerInstallationRepository {
}
Future<void> saveDomain(final ServerDomain serverDomain) async {
await getIt<ApiConfigModel>().storeServerDomain(serverDomain);
await getIt<ApiConfigModel>().setServerDomain(serverDomain);
}
Future<void> deleteDomain() async {

View File

@ -1,123 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.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/json/server_job.dart';
export 'package:provider/provider.dart';
part 'server_jobs_state.dart';
class ServerJobsCubit
extends ServerInstallationDependendCubit<ServerJobsState> {
ServerJobsCubit(final ServerInstallationCubit serverInstallationCubit)
: super(
serverInstallationCubit,
ServerJobsState(),
);
Timer? timer;
final ServerApi api = ServerApi();
@override
void clear() async {
emit(
ServerJobsState(),
);
if (timer != null && timer!.isActive) {
timer!.cancel();
timer = null;
}
}
@override
void load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
final List<ServerJob> jobs = await api.getServerJobs();
emit(
ServerJobsState(
serverJobList: jobs,
),
);
timer = Timer(const Duration(seconds: 5), () => reload(useTimer: true));
}
}
Future<void> migrateToBinds(final Map<String, String> serviceToDisk) async {
final result = await api.migrateToBinds(serviceToDisk);
if (result.data == null) {
getIt<NavigationService>().showSnackBar(result.message!);
return;
}
emit(
ServerJobsState(
migrationJobUid: result.data,
),
);
}
ServerJob? getServerJobByUid(final String uid) {
ServerJob? job;
try {
job = state.serverJobList.firstWhere(
(final ServerJob job) => job.uid == uid,
);
} catch (e) {
print(e);
}
return job;
}
Future<void> removeServerJob(final String uid) async {
final result = await api.removeApiJob(uid);
if (!result.success) {
getIt<NavigationService>().showSnackBar('jobs.generic_error'.tr());
return;
}
if (!result.data) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
return;
}
emit(
ServerJobsState(
serverJobList: [
for (final ServerJob job in state.serverJobList)
if (job.uid != uid) job,
],
),
);
}
Future<void> removeAllFinishedJobs() async {
final List<ServerJob> finishedJobs = state.serverJobList
.where(
(final ServerJob job) =>
job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error,
)
.toList();
for (final ServerJob job in finishedJobs) {
await removeServerJob(job.uid);
}
}
Future<void> reload({final bool useTimer = false}) async {
final List<ServerJob> jobs = await api.getServerJobs();
emit(
ServerJobsState(
serverJobList: jobs,
),
);
if (useTimer) {
timer = Timer(const Duration(seconds: 5), () => reload(useTimer: true));
}
}
}

View File

@ -1,49 +0,0 @@
part of 'server_jobs_cubit.dart';
class ServerJobsState extends ServerInstallationDependendState {
ServerJobsState({
final serverJobList = const <ServerJob>[],
this.migrationJobUid,
}) {
_serverJobList = serverJobList;
}
late final List<ServerJob> _serverJobList;
final String? migrationJobUid;
List<ServerJob> get serverJobList {
try {
final List<ServerJob> list = _serverJobList;
list.sort((final a, final b) => b.createdAt.compareTo(a.createdAt));
return list;
} on UnsupportedError {
return _serverJobList;
}
}
List<ServerJob> get backupJobList => serverJobList
.where(
// The backup jobs has the format of 'service.<service_id>.backup'
(final job) =>
job.typeId.contains('backup') || job.typeId.contains('restore'),
)
.toList();
bool get hasRemovableJobs => serverJobList.any(
(final job) =>
job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error,
);
@override
List<Object?> get props => [migrationJobUid, _serverJobList];
ServerJobsState copyWith({
final List<ServerJob>? serverJobList,
final String? migrationJobUid,
}) =>
ServerJobsState(
serverJobList: serverJobList ?? _serverJobList,
migrationJobUid: migrationJobUid ?? this.migrationJobUid,
);
}

View File

@ -1,78 +0,0 @@
import 'dart:async';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/cubit/provider_volumes/provider_volume_cubit.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/json/server_disk_volume.dart';
part 'server_volume_state.dart';
class ApiServerVolumeCubit
extends ServerInstallationDependendCubit<ApiServerVolumeState> {
ApiServerVolumeCubit(
final ServerInstallationCubit serverInstallationCubit,
this.providerVolumeCubit,
) : super(serverInstallationCubit, ApiServerVolumeState.initial()) {
_providerVolumeSubscription =
providerVolumeCubit.stream.listen(checkProviderVolumes);
}
final ServerApi serverApi = ServerApi();
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
unawaited(reload());
}
}
late StreamSubscription<ApiProviderVolumeState> _providerVolumeSubscription;
final ApiProviderVolumeCubit providerVolumeCubit;
void checkProviderVolumes(final ApiProviderVolumeState state) {
emit(
ApiServerVolumeState(
this.state._volumes,
this.state.status,
this.state.usesBinds,
DiskStatus.fromVolumes(this.state._volumes, state.volumes),
),
);
return;
}
Future<void> reload() async {
final volumes = await serverApi.getServerDiskVolumes();
final usesBinds = await serverApi.isUsingBinds();
var status = LoadingStatus.error;
if (volumes.isNotEmpty) {
status = LoadingStatus.success;
}
emit(
ApiServerVolumeState(
volumes,
status,
usesBinds,
DiskStatus.fromVolumes(
volumes,
providerVolumeCubit.state.volumes,
),
),
);
}
@override
void clear() {
emit(ApiServerVolumeState.initial());
}
@override
Future<void> close() {
_providerVolumeSubscription.cancel();
return super.close();
}
}

View File

@ -1,42 +0,0 @@
part of 'server_volume_cubit.dart';
class ApiServerVolumeState extends ServerInstallationDependendState {
const ApiServerVolumeState(
this._volumes,
this.status,
this.usesBinds,
this._diskStatus,
);
ApiServerVolumeState.initial()
: this(const [], LoadingStatus.uninitialized, null, DiskStatus());
final List<ServerDiskVolume> _volumes;
final DiskStatus _diskStatus;
final bool? usesBinds;
final LoadingStatus status;
List<DiskVolume> get volumes => _diskStatus.diskVolumes;
DiskStatus get diskStatus => _diskStatus;
DiskVolume getVolume(final String volumeName) => volumes.firstWhere(
(final volume) => volume.name == volumeName,
orElse: () => DiskVolume(),
);
ApiServerVolumeState copyWith({
final List<ServerDiskVolume>? volumes,
final LoadingStatus? status,
final bool? usesBinds,
final DiskStatus? diskStatus,
}) =>
ApiServerVolumeState(
volumes ?? _volumes,
status ?? this.status,
usesBinds ?? this.usesBinds,
diskStatus ?? _diskStatus,
);
@override
List<Object?> get props => [_volumes, status, usesBinds];
}

View File

@ -1,83 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.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/service.dart';
part 'services_state.dart';
class ServicesCubit extends ServerInstallationDependendCubit<ServicesState> {
ServicesCubit(final ServerInstallationCubit serverInstallationCubit)
: super(serverInstallationCubit, const ServicesState.empty());
final ServerApi api = ServerApi();
Timer? timer;
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
final List<Service> services = await api.getAllServices();
emit(
ServicesState(
services: services,
lockedServices: const [],
),
);
timer = Timer(const Duration(seconds: 10), () => reload(useTimer: true));
}
}
Future<void> reload({final bool useTimer = false}) async {
final List<Service> services = await api.getAllServices();
emit(
state.copyWith(
services: services,
),
);
if (useTimer) {
timer = Timer(const Duration(seconds: 60), () => reload(useTimer: true));
}
}
Future<void> restart(final String serviceId) async {
emit(state.copyWith(lockedServices: [...state.lockedServices, serviceId]));
final result = await api.restartService(serviceId);
if (!result.success) {
getIt<NavigationService>().showSnackBar('jobs.generic_error'.tr());
return;
}
if (!result.data) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
return;
}
await Future.delayed(const Duration(seconds: 2));
unawaited(reload());
await Future.delayed(const Duration(seconds: 10));
emit(
state.copyWith(
lockedServices: state.lockedServices
.where((final element) => element != serviceId)
.toList(),
),
);
unawaited(reload());
}
Future<void> moveService(
final String serviceId,
final String destination,
) async {
await api.moveService(serviceId, destination);
}
@override
void clear() async {
emit(const ServicesState.empty());
if (timer != null && timer!.isActive) {
timer!.cancel();
timer = null;
}
}
}

View File

@ -1,49 +0,0 @@
part of 'services_cubit.dart';
class ServicesState extends ServerInstallationDependendState {
const ServicesState({
required this.services,
required this.lockedServices,
});
const ServicesState.empty()
: this(services: const [], lockedServices: const []);
final List<Service> services;
final List<String> lockedServices;
List<Service> get servicesThatCanBeBackedUp => services
.where(
(final service) => service.canBeBackedUp,
)
.toList();
bool isServiceLocked(final String serviceId) =>
lockedServices.contains(serviceId);
Service? getServiceById(final String id) {
final service = services.firstWhere(
(final service) => service.id == id,
orElse: () => Service.empty,
);
if (service.id == 'empty') {
return null;
}
return service;
}
@override
List<Object> get props => [
services,
lockedServices,
];
ServicesState copyWith({
final List<Service>? services,
final List<String>? lockedServices,
}) =>
ServicesState(
services: services ?? this.services,
lockedServices: lockedServices ?? this.lockedServices,
);
}

View File

@ -1,186 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:hive/hive.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.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/user.dart';
export 'package:provider/provider.dart';
part 'users_state.dart';
class UsersCubit extends ServerInstallationDependendCubit<UsersState> {
UsersCubit(final ServerInstallationCubit serverInstallationCubit)
: super(
serverInstallationCubit,
const UsersState(
<User>[],
false,
),
);
Box<User> box = Hive.box<User>(BNames.usersBox);
Box serverInstallationBox = Hive.box(BNames.serverInstallationBox);
final ServerApi api = ServerApi();
@override
Future<void> load() async {
if (serverInstallationCubit.state is! ServerInstallationFinished) {
return;
}
final List<User> loadedUsers = box.values.toList();
if (loadedUsers.isNotEmpty) {
emit(
UsersState(
loadedUsers,
false,
),
);
}
unawaited(refresh());
}
Future<void> refresh() async {
if (serverInstallationCubit.state is! ServerInstallationFinished) {
return;
}
emit(state.copyWith(isLoading: true));
final List<User> usersFromServer = await api.getAllUsers();
if (usersFromServer.isNotEmpty) {
emit(
UsersState(
usersFromServer,
false,
),
);
// Update the users it the box
await box.clear();
await box.addAll(usersFromServer);
} else {
getIt<NavigationService>()
.showSnackBar('users.could_not_fetch_users'.tr());
emit(state.copyWith(isLoading: false));
}
}
Future<void> createUser(final User user) async {
// If user exists on server, do nothing
if (state.users
.any((final User u) => u.login == user.login && u.isFoundOnServer)) {
return;
}
final String? password = user.password;
if (password == null) {
getIt<NavigationService>()
.showSnackBar('users.could_not_create_user'.tr());
return;
}
// If API returned error, do nothing
final GenericResult<User?> result =
await api.createUser(user.login, password);
if (result.data == null) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'users.could_not_create_user'.tr());
return;
}
final List<User> loadedUsers = List<User>.from(state.users);
loadedUsers.add(result.data!);
await box.clear();
await box.addAll(loadedUsers);
emit(state.copyWith(users: loadedUsers));
}
Future<void> deleteUser(final User user) async {
// If user is primary or root, don't delete
if (user.type != UserType.normal) {
getIt<NavigationService>()
.showSnackBar('users.could_not_delete_user'.tr());
return;
}
final List<User> loadedUsers = List<User>.from(state.users);
final GenericResult result = await api.deleteUser(user.login);
if (result.success && result.data) {
loadedUsers.removeWhere((final User u) => u.login == user.login);
await box.clear();
await box.addAll(loadedUsers);
emit(state.copyWith(users: loadedUsers));
}
if (!result.success) {
getIt<NavigationService>().showSnackBar('jobs.generic_error'.tr());
}
if (!result.data) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'jobs.generic_error'.tr());
}
}
Future<void> changeUserPassword(
final User user,
final String newPassword,
) async {
if (user.type == UserType.root) {
getIt<NavigationService>()
.showSnackBar('users.could_not_change_password'.tr());
return;
}
final GenericResult<User?> result =
await api.updateUser(user.login, newPassword);
if (result.data == null) {
getIt<NavigationService>().showSnackBar(
result.message ?? 'users.could_not_change_password'.tr(),
);
}
}
Future<void> addSshKey(final User user, final String publicKey) async {
final GenericResult<User?> result =
await api.addSshKey(user.login, publicKey);
if (result.data != null) {
final User updatedUser = result.data!;
final int index =
state.users.indexWhere((final User u) => u.login == user.login);
await box.putAt(index, updatedUser);
emit(
state.copyWith(
users: box.values.toList(),
),
);
} else {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'users.could_not_add_ssh_key'.tr());
}
}
Future<void> deleteSshKey(final User user, final String publicKey) async {
final GenericResult<User?> result =
await api.removeSshKey(user.login, publicKey);
if (result.data != null) {
final User updatedUser = result.data!;
final int index =
state.users.indexWhere((final User u) => u.login == user.login);
await box.putAt(index, updatedUser);
emit(
state.copyWith(
users: box.values.toList(),
),
);
}
}
@override
void clear() async {
emit(
const UsersState(
<User>[],
false,
),
);
}
}

View File

@ -42,47 +42,47 @@ class ApiConfigModel {
_serverProvider = value;
}
Future<void> storeDnsProviderType(final DnsProviderType value) async {
Future<void> setDnsProviderType(final DnsProviderType value) async {
await _box.put(BNames.dnsProvider, value);
_dnsProvider = value;
}
Future<void> storeServerProviderKey(final String value) async {
Future<void> setServerProviderKey(final String value) async {
await _box.put(BNames.hetznerKey, value);
_serverProviderKey = value;
}
Future<void> storeDnsProviderKey(final String value) async {
Future<void> setDnsProviderKey(final String value) async {
await _box.put(BNames.cloudFlareKey, value);
_dnsProviderKey = value;
}
Future<void> storeServerTypeIdentifier(final String typeIdentifier) async {
Future<void> setServerTypeIdentifier(final String typeIdentifier) async {
await _box.put(BNames.serverTypeIdentifier, typeIdentifier);
_serverType = typeIdentifier;
}
Future<void> storeServerLocation(final String serverLocation) async {
Future<void> setServerLocation(final String serverLocation) async {
await _box.put(BNames.serverLocation, serverLocation);
_serverLocation = serverLocation;
}
Future<void> storeBackblazeCredential(final BackupsCredential value) async {
Future<void> setBackblazeCredential(final BackupsCredential value) async {
await _box.put(BNames.backblazeCredential, value);
_backblazeCredential = value;
}
Future<void> storeServerDomain(final ServerDomain value) async {
Future<void> setServerDomain(final ServerDomain value) async {
await _box.put(BNames.serverDomain, value);
_serverDomain = value;
}
Future<void> storeServerDetails(final ServerHostingDetails value) async {
Future<void> setServerDetails(final ServerHostingDetails value) async {
await _box.put(BNames.serverDetails, value);
_serverDetails = value;
}
Future<void> storeBackblazeBucket(final BackblazeBucket value) async {
Future<void> setBackblazeBucket(final BackblazeBucket value) async {
await _box.put(BNames.backblazeBucket, value);
_backblazeBucket = value;
}

View File

@ -0,0 +1,435 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:hive/hive.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/config/hive_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/server_details.dart';
import 'package:selfprivacy/logic/models/hive/server_domain.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
import 'package:selfprivacy/logic/models/json/server_disk_volume.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/logic/models/system_settings.dart';
/// Repository for all API calls
/// Stores the current state of all data from API and exposes it to Blocs.
class ApiConnectionRepository {
Box box = Hive.box(BNames.serverInstallationBox);
final ServerApi api = ServerApi();
final ApiData _apiData = ApiData(ServerApi());
ApiData get apiData => _apiData;
ConnectionStatus connectionStatus = ConnectionStatus.nonexistent;
final _dataStream = StreamController<ApiData>.broadcast();
final _connectionStatusStream =
StreamController<ConnectionStatus>.broadcast();
Stream<ApiData> get dataStream => _dataStream.stream;
Stream<ConnectionStatus> get connectionStatusStream =>
_connectionStatusStream.stream;
ConnectionStatus get currentConnectionStatus => connectionStatus;
Timer? _timer;
Future<void> removeServerJob(final String uid) async {
await api.removeApiJob(uid);
_apiData.serverJobs.data
?.removeWhere((final ServerJob element) => element.uid == uid);
_dataStream.add(_apiData);
}
Future<void> removeAllFinishedServerJobs() async {
final List<ServerJob> finishedJobs = _apiData.serverJobs.data
?.where(
(final ServerJob element) =>
element.status == JobStatusEnum.finished ||
element.status == JobStatusEnum.error,
)
.toList() ??
[];
// Optimistically remove the jobs from the list
_apiData.serverJobs.data?.removeWhere(
(final ServerJob element) =>
element.status == JobStatusEnum.finished ||
element.status == JobStatusEnum.error,
);
_dataStream.add(_apiData);
await Future.forEach<ServerJob>(
finishedJobs,
(final ServerJob job) async => removeServerJob(job.uid),
);
}
Future<(bool, String)> createUser(final User user) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return (false, 'basis.network_error'.tr());
}
// If user exists on server, do nothing
if (loadedUsers
.any((final User u) => u.login == user.login && u.isFoundOnServer)) {
return (false, 'users.user_already_exists'.tr());
}
final String? password = user.password;
if (password == null) {
return (false, 'users.could_not_create_user'.tr());
}
// If API returned error, do nothing
final GenericResult<User?> result =
await api.createUser(user.login, password);
if (result.data == null) {
return (false, result.message ?? 'users.could_not_create_user'.tr());
}
_apiData.users.data?.add(result.data!);
_apiData.users.invalidate();
return (true, result.message ?? 'basis.done'.tr());
}
Future<(bool, String)> deleteUser(final User user) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return (false, 'basis.network_error'.tr());
}
// If user is primary or root, don't delete
if (user.type != UserType.normal) {
return (false, 'users.could_not_delete_user'.tr());
}
final GenericResult result = await api.deleteUser(user.login);
if (result.success && result.data) {
_apiData.users.data?.removeWhere((final User u) => u.login == user.login);
_apiData.users.invalidate();
}
if (!result.success || !result.data) {
return (false, result.message ?? 'jobs.generic_error'.tr());
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<(bool, String)> changeUserPassword(
final User user,
final String newPassword,
) async {
if (user.type == UserType.root) {
return (false, 'users.could_not_change_password'.tr());
}
final GenericResult<User?> result = await api.updateUser(
user.login,
newPassword,
);
if (result.data == null) {
getIt<NavigationService>().showSnackBar(
result.message ?? 'users.could_not_change_password'.tr(),
);
return (
false,
result.message ?? 'users.could_not_change_password'.tr(),
);
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<(bool, String)> addSshKey(
final User user,
final String publicKey,
) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return (false, 'basis.network_error'.tr());
}
final GenericResult<User?> result =
await api.addSshKey(user.login, publicKey);
if (result.data != null) {
final User updatedUser = result.data!;
final int index =
loadedUsers.indexWhere((final User u) => u.login == user.login);
loadedUsers[index] = updatedUser;
_apiData.users.invalidate();
} else {
return (false, result.message ?? 'users.could_not_add_ssh_key'.tr());
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<(bool, String)> deleteSshKey(
final User user,
final String publicKey,
) async {
final List<User>? loadedUsers = _apiData.users.data;
if (loadedUsers == null) {
return (false, 'basis.network_error'.tr());
}
final GenericResult<User?> result =
await api.removeSshKey(user.login, publicKey);
if (result.data != null) {
final User updatedUser = result.data!;
final int index =
loadedUsers.indexWhere((final User u) => u.login == user.login);
loadedUsers[index] = updatedUser;
_apiData.users.invalidate();
} else {
return (false, result.message ?? 'jobs.generic_error'.tr());
}
return (true, result.message ?? 'basis.done'.tr());
}
Future<(bool, String)> setAutoUpgradeSettings(
final bool enable,
final bool allowReboot,
) async {
final GenericResult<AutoUpgradeSettings?> result =
await api.setAutoUpgradeSettings(
AutoUpgradeSettings(
enable: enable,
allowReboot: allowReboot,
),
);
_apiData.settings.invalidate();
if (result.data != null) {
return (true, result.message ?? 'basis.done'.tr());
} else {
return (false, result.message ?? 'jobs.generic_error'.tr());
}
}
Future<(bool, String)> setServerTimezone(
final String timezone,
) async {
final GenericResult result = await api.setTimezone(timezone);
_apiData.settings.invalidate();
if (result.success) {
return (true, result.message ?? 'basis.done'.tr());
} else {
return (false, result.message ?? 'jobs.generic_error'.tr());
}
}
void dispose() {
_dataStream.close();
_connectionStatusStream.close();
_timer?.cancel();
}
ServerHostingDetails? get serverDetails =>
getIt<ApiConfigModel>().serverDetails;
ServerDomain? get serverDomain => getIt<ApiConfigModel>().serverDomain;
void init() async {
final serverDetails = getIt<ApiConfigModel>().serverDetails;
final hasFinalChecked =
box.get(BNames.hasFinalChecked, defaultValue: false);
if (serverDetails == null || !hasFinalChecked) {
return;
}
connectionStatus = ConnectionStatus.reconnecting;
_connectionStatusStream.add(connectionStatus);
final String? apiVersion = await api.getApiVersion();
if (apiVersion == null) {
connectionStatus = ConnectionStatus.offline;
_connectionStatusStream.add(connectionStatus);
return;
} else {
_apiData.apiVersion.data = apiVersion;
_dataStream.add(_apiData);
}
await _refetchEverything(Version.parse(apiVersion));
connectionStatus = ConnectionStatus.connected;
_connectionStatusStream.add(connectionStatus);
// Use timer to periodically check for new jobs
_timer = Timer.periodic(
const Duration(seconds: 10),
reload,
);
}
Future<void> _refetchEverything(final Version version) async {
await _apiData.serverJobs
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.backups
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.backupConfig
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.services
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.volumes
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.recoveryKeyStatus
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.devices
.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.users.refetchData(version, () => _dataStream.add(_apiData));
await _apiData.settings
.refetchData(version, () => _dataStream.add(_apiData));
}
Future<void> reload(final Timer? timer) async {
final serverDetails = getIt<ApiConfigModel>().serverDetails;
if (serverDetails == null) {
return;
}
final String? apiVersion = await api.getApiVersion();
if (apiVersion == null) {
connectionStatus = ConnectionStatus.offline;
_connectionStatusStream.add(connectionStatus);
return;
} else {
connectionStatus = ConnectionStatus.connected;
_connectionStatusStream.add(connectionStatus);
_apiData.apiVersion.data = apiVersion;
}
final Version version = Version.parse(apiVersion);
await _refetchEverything(version);
}
void emitData() {
_dataStream.add(_apiData);
}
}
class ApiData {
ApiData(final ServerApi api)
: apiVersion = ApiDataElement<String>(
fetchData: () async => api.getApiVersion(),
),
serverJobs = ApiDataElement<List<ServerJob>>(
fetchData: () async => api.getServerJobs(),
ttl: 10,
),
backupConfig = ApiDataElement<BackupConfiguration>(
fetchData: () async => api.getBackupsConfiguration(),
requiredApiVersion: '>=2.4.2',
ttl: 120,
),
backups = ApiDataElement<List<Backup>>(
fetchData: () async => api.getBackups(),
requiredApiVersion: '>=2.4.2',
ttl: 120,
),
services = ApiDataElement<List<Service>>(
fetchData: () async => api.getAllServices(),
requiredApiVersion: '>=2.4.3',
),
volumes = ApiDataElement<List<ServerDiskVolume>>(
fetchData: () async => api.getServerDiskVolumes(),
),
recoveryKeyStatus = ApiDataElement<RecoveryKeyStatus>(
fetchData: () async => (await api.getRecoveryTokenStatus()).data,
ttl: 300,
),
devices = ApiDataElement<List<ApiToken>>(
fetchData: () async => (await api.getApiTokens()).data,
),
users = ApiDataElement<List<User>>(
fetchData: () async => api.getAllUsers(),
),
settings = ApiDataElement<SystemSettings>(
fetchData: () async => api.getSystemSettings(),
ttl: 600,
);
ApiDataElement<List<ServerJob>> serverJobs;
ApiDataElement<String> apiVersion;
ApiDataElement<BackupConfiguration> backupConfig;
ApiDataElement<List<Backup>> backups;
ApiDataElement<List<Service>> services;
ApiDataElement<List<ServerDiskVolume>> volumes;
ApiDataElement<RecoveryKeyStatus> recoveryKeyStatus;
ApiDataElement<List<ApiToken>> devices;
ApiDataElement<List<User>> users;
ApiDataElement<SystemSettings> settings;
}
enum ConnectionStatus {
nonexistent,
connected,
reconnecting,
offline,
unauthorized,
}
class ApiDataElement<T> {
ApiDataElement({
required this.fetchData,
final T? data,
this.requiredApiVersion = '>=2.3.0',
this.ttl = 60,
}) : _data = data,
_lastUpdated = DateTime.now();
T? _data;
final String requiredApiVersion;
final Future<T?> Function() fetchData;
Future<void> refetchData(
final Version version,
final Function callback,
) async {
if (VersionConstraint.parse(requiredApiVersion).allows(version)) {
if (isExpired || _data == null) {
final newData = await fetchData();
if (T is List) {
if (Object.hashAll(newData as Iterable<Object?>) !=
Object.hashAll(_data as Iterable<Object?>)) {
_data = [...newData] as T?;
}
} else {
if (newData.hashCode != _data.hashCode) {
_data = newData;
}
}
callback();
}
}
}
/// TTL of the data in seconds
final int ttl;
Type get type => T;
void invalidate() {
_lastUpdated = DateTime.fromMillisecondsSinceEpoch(0);
}
/// Timestamp of when the data was last updated
DateTime _lastUpdated;
bool get isExpired {
final now = DateTime.now();
final difference = now.difference(_lastUpdated);
return difference.inSeconds > ttl;
}
T? get data => _data;
/// Sets the data and updates the lastUpdated timestamp
set data(final T? data) {
_data = data;
_lastUpdated = DateTime.now();
}
/// Returns the last time the data was updated
DateTime get lastUpdated => _lastUpdated;
}

View File

@ -1,12 +0,0 @@
import 'package:flutter/material.dart';
class TimerModel extends ChangeNotifier {
DateTime _time = DateTime.now();
DateTime get time => _time;
void restart() {
_time = DateTime.now();
notifyListeners();
}
}

View File

@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/backups.graphql.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart';
@ -40,7 +41,7 @@ extension BackupReasonExtension on Enum$BackupReason {
};
}
class BackupConfiguration {
class BackupConfiguration extends Equatable {
BackupConfiguration.fromGraphQL(
final Query$BackupConfiguration$backup$configuration configuration,
) : this(
@ -58,7 +59,7 @@ class BackupConfiguration {
),
);
BackupConfiguration({
const BackupConfiguration({
required this.autobackupPeriod,
required this.encryptionKey,
required this.isInitialized,
@ -75,9 +76,39 @@ class BackupConfiguration {
final String? locationName;
final BackupsProviderType provider;
final AutobackupQuotas autobackupQuotas;
@override
List<Object?> get props => [
autobackupPeriod,
encryptionKey,
isInitialized,
locationId,
locationName,
provider,
autobackupQuotas,
];
BackupConfiguration copyWith({
final Duration? autobackupPeriod,
final String? encryptionKey,
final bool? isInitialized,
final String? locationId,
final String? locationName,
final BackupsProviderType? provider,
final AutobackupQuotas? autobackupQuotas,
}) =>
BackupConfiguration(
autobackupPeriod: autobackupPeriod ?? this.autobackupPeriod,
encryptionKey: encryptionKey ?? this.encryptionKey,
isInitialized: isInitialized ?? this.isInitialized,
locationId: locationId ?? this.locationId,
locationName: locationName ?? this.locationName,
provider: provider ?? this.provider,
autobackupQuotas: autobackupQuotas ?? this.autobackupQuotas,
);
}
class AutobackupQuotas {
class AutobackupQuotas extends Equatable {
AutobackupQuotas.fromGraphQL(
final Query$BackupConfiguration$backup$configuration$autobackupQuotas
autobackupQuotas,
@ -89,7 +120,7 @@ class AutobackupQuotas {
yearly: autobackupQuotas.yearly,
);
AutobackupQuotas({
const AutobackupQuotas({
required this.last,
required this.daily,
required this.weekly,
@ -117,6 +148,15 @@ class AutobackupQuotas {
monthly: monthly ?? this.monthly,
yearly: yearly ?? this.yearly,
);
@override
List<Object?> get props => [
last,
daily,
weekly,
monthly,
yearly,
];
}
enum BackupRestoreStrategy {

View File

@ -9,13 +9,12 @@ class DiskVolume {
this.sizeUsed = const DiskSize(byte: 0),
this.root = false,
this.isResizable = false,
this.serverDiskVolume,
this.providerVolume,
});
DiskVolume.fromServerDiscVolume(
final ServerDiskVolume volume,
final ServerVolume? providerVolume,
final ServerProviderVolume? providerVolume,
) : this(
name: volume.name,
sizeTotal: DiskSize(
@ -27,7 +26,6 @@ class DiskVolume {
),
root: volume.root,
isResizable: providerVolume != null,
serverDiskVolume: volume,
providerVolume: providerVolume,
);
@ -51,8 +49,7 @@ class DiskVolume {
String name;
bool root;
bool isResizable;
ServerDiskVolume? serverDiskVolume;
ServerVolume? providerVolume;
ServerProviderVolume? providerVolume;
/// from 0.0 to 1.0
double get percentage =>
@ -67,7 +64,7 @@ class DiskVolume {
final bool? root,
final bool? isResizable,
final ServerDiskVolume? serverDiskVolume,
final ServerVolume? providerVolume,
final ServerProviderVolume? providerVolume,
}) =>
DiskVolume(
sizeUsed: sizeUsed ?? this.sizeUsed,
@ -75,7 +72,6 @@ class DiskVolume {
name: name ?? this.name,
root: root ?? this.root,
isResizable: isResizable ?? this.isResizable,
serverDiskVolume: serverDiskVolume ?? this.serverDiskVolume,
providerVolume: providerVolume ?? this.providerVolume,
);
}
@ -83,14 +79,15 @@ class DiskVolume {
class DiskStatus {
DiskStatus.fromVolumes(
final List<ServerDiskVolume> serverVolumes,
final List<ServerVolume> providerVolumes,
final List<ServerProviderVolume> providerVolumes,
) {
diskVolumes = serverVolumes.map((
final ServerDiskVolume volume,
) {
ServerVolume? providerVolume;
ServerProviderVolume? providerVolume;
for (final ServerVolume iterableProviderVolume in providerVolumes) {
for (final ServerProviderVolume iterableProviderVolume
in providerVolumes) {
if (iterableProviderVolume.linuxDevice == null ||
volume.model == null ||
volume.serial == null) {

View File

@ -29,4 +29,19 @@ class BackblazeBucket {
@override
String toString() => bucketName;
BackblazeBucket copyWith({
final String? bucketId,
final String? applicationKeyId,
final String? applicationKey,
final String? bucketName,
final String? encryptionKey,
}) =>
BackblazeBucket(
bucketId: bucketId ?? this.bucketId,
applicationKeyId: applicationKeyId ?? this.applicationKeyId,
applicationKey: applicationKey ?? this.applicationKey,
bucketName: bucketName ?? this.bucketName,
encryptionKey: encryptionKey ?? this.encryptionKey,
);
}

View File

@ -27,8 +27,9 @@ class ServerHostingDetails {
@HiveField(2)
final DateTime? startTime;
// TODO: Check if it is still needed
@HiveField(4)
final ServerVolume volume;
final ServerProviderVolume volume;
@HiveField(5)
final String apiToken;
@ -52,8 +53,8 @@ class ServerHostingDetails {
}
@HiveType(typeId: 5)
class ServerVolume {
ServerVolume({
class ServerProviderVolume {
ServerProviderVolume({
required this.id,
required this.name,
required this.sizeByte,

View File

@ -20,7 +20,7 @@ class ServerHostingDetailsAdapter extends TypeAdapter<ServerHostingDetails> {
ip4: fields[0] as String,
id: fields[1] as int,
createTime: fields[3] as DateTime?,
volume: fields[4] as ServerVolume,
volume: fields[4] as ServerProviderVolume,
apiToken: fields[5] as String,
provider: fields[6] == null
? ServerProviderType.hetzner
@ -60,17 +60,17 @@ class ServerHostingDetailsAdapter extends TypeAdapter<ServerHostingDetails> {
typeId == other.typeId;
}
class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
class ServerProviderVolumeAdapter extends TypeAdapter<ServerProviderVolume> {
@override
final int typeId = 5;
@override
ServerVolume read(BinaryReader reader) {
ServerProviderVolume read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return ServerVolume(
return ServerProviderVolume(
id: fields[1] as int,
name: fields[2] as String,
sizeByte: fields[3] == null ? 10737418240 : fields[3] as int,
@ -81,7 +81,7 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
}
@override
void write(BinaryWriter writer, ServerVolume obj) {
void write(BinaryWriter writer, ServerProviderVolume obj) {
writer
..writeByte(6)
..writeByte(1)
@ -104,7 +104,7 @@ class ServerVolumeAdapter extends TypeAdapter<ServerVolume> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ServerVolumeAdapter &&
other is ServerProviderVolumeAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@ -1,8 +1,9 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/models/hive/user.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/utils/password_generator.dart';
@ -11,69 +12,148 @@ abstract class ClientJob extends Equatable {
ClientJob({
required this.title,
final String? id,
this.requiresRebuild = true,
this.status = JobStatusEnum.created,
this.message,
}) : id = id ?? StringGenerators.simpleId();
final String title;
final String id;
final bool requiresRebuild;
final JobStatusEnum status;
final String? message;
bool canAddTo(final List<ClientJob> jobs) => true;
void execute(final JobsCubit cubit);
Future<(bool, String)> execute();
@override
List<Object> get props => [id, title];
List<Object> get props => [id, title, status];
ClientJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
});
}
class RebuildServerJob extends ClientJob {
RebuildServerJob({
required super.title,
class UpgradeServerJob extends ClientJob {
UpgradeServerJob({
super.status,
super.message,
super.id,
});
}) : super(title: 'jobs.start_server_upgrade'.tr());
@override
bool canAddTo(final List<ClientJob> jobs) =>
!jobs.any((final job) => job is RebuildServerJob);
!jobs.any((final job) => job is UpgradeServerJob);
@override
void execute(final JobsCubit cubit) async {
await cubit.upgradeServer();
}
Future<(bool, String)> execute() async => (false, 'unimplemented');
@override
UpgradeServerJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
UpgradeServerJob(
status: status,
message: message,
id: id,
);
}
class RebootServerJob extends ClientJob {
RebootServerJob({
super.status,
super.message,
super.id,
}) : super(title: 'jobs.reboot_server'.tr(), requiresRebuild: false);
@override
bool canAddTo(final List<ClientJob> jobs) =>
!jobs.any((final job) => job is RebootServerJob);
@override
Future<(bool, String)> execute() async => (false, 'unimplemented');
@override
RebootServerJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
RebootServerJob(
status: status,
message: message,
id: id,
);
}
class CreateUserJob extends ClientJob {
CreateUserJob({
required this.user,
super.status,
super.message,
super.id,
}) : super(title: '${"jobs.create_user".tr()} ${user.login}');
final User user;
@override
void execute(final JobsCubit cubit) async {
await cubit.usersCubit.createUser(user);
}
Future<(bool, String)> execute() async =>
getIt<ApiConnectionRepository>().createUser(user);
@override
List<Object> get props => [id, title, user];
List<Object> get props => [...super.props, user];
@override
CreateUserJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
CreateUserJob(
user: user,
status: status,
message: message,
id: id,
);
}
class ResetUserPasswordJob extends ClientJob {
ResetUserPasswordJob({
required this.user,
super.status,
super.message,
super.id,
}) : super(title: '${"jobs.reset_user_password".tr()} ${user.login}');
final User user;
@override
void execute(final JobsCubit cubit) async {
await cubit.usersCubit.changeUserPassword(user, user.password!);
}
Future<(bool, String)> execute() async =>
getIt<ApiConnectionRepository>().changeUserPassword(user, user.password!);
@override
List<Object> get props => [id, title, user];
List<Object> get props => [...super.props, user];
@override
ResetUserPasswordJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ResetUserPasswordJob(
user: user,
status: status,
message: message,
id: id,
);
}
class DeleteUserJob extends ClientJob {
DeleteUserJob({
required this.user,
super.status,
super.message,
super.id,
}) : super(title: '${"jobs.delete_user".tr()} ${user.login}');
final User user;
@ -84,18 +164,32 @@ class DeleteUserJob extends ClientJob {
);
@override
void execute(final JobsCubit cubit) async {
await cubit.usersCubit.deleteUser(user);
}
Future<(bool, String)> execute() async =>
getIt<ApiConnectionRepository>().deleteUser(user);
@override
List<Object> get props => [id, title, user];
List<Object> get props => [...super.props, user];
@override
DeleteUserJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
DeleteUserJob(
user: user,
status: status,
message: message,
id: id,
);
}
class ServiceToggleJob extends ClientJob {
ServiceToggleJob({
required this.service,
required this.needToTurnOn,
super.status,
super.message,
super.id,
}) : super(
title:
'${needToTurnOn ? "jobs.service_turn_on".tr() : "jobs.service_turn_off".tr()} ${service.displayName}',
@ -110,36 +204,70 @@ class ServiceToggleJob extends ClientJob {
);
@override
void execute(final JobsCubit cubit) async {
await cubit.api.switchService(service.id, needToTurnOn);
Future<(bool, String)> execute() async {
final result = await getIt<ApiConnectionRepository>()
.api
.switchService(service.id, needToTurnOn);
return (result.success, result.message ?? 'jobs.generic_error'.tr());
}
@override
List<Object> get props => [...super.props, service];
@override
ServiceToggleJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ServiceToggleJob(
service: service,
needToTurnOn: needToTurnOn,
status: status,
message: message,
id: id,
);
}
class CreateSSHKeyJob extends ClientJob {
CreateSSHKeyJob({
required this.user,
required this.publicKey,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.create_ssh_key'.tr(args: [user.login]));
final User user;
final String publicKey;
@override
void execute(final JobsCubit cubit) async {
await cubit.usersCubit.addSshKey(user, publicKey);
}
Future<(bool, String)> execute() async =>
getIt<ApiConnectionRepository>().addSshKey(user, publicKey);
@override
List<Object> get props => [id, title, user, publicKey];
List<Object> get props => [...super.props, user, publicKey];
@override
CreateSSHKeyJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
CreateSSHKeyJob(
user: user,
publicKey: publicKey,
status: status,
message: message,
id: id,
);
}
class DeleteSSHKeyJob extends ClientJob {
DeleteSSHKeyJob({
required this.user,
required this.publicKey,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.delete_ssh_key'.tr(args: [user.login]));
final User user;
@ -154,10 +282,120 @@ class DeleteSSHKeyJob extends ClientJob {
);
@override
void execute(final JobsCubit cubit) async {
await cubit.usersCubit.deleteSshKey(user, publicKey);
Future<(bool, String)> execute() async =>
getIt<ApiConnectionRepository>().deleteSshKey(user, publicKey);
@override
List<Object> get props => [...super.props, user, publicKey];
@override
DeleteSSHKeyJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
DeleteSSHKeyJob(
user: user,
publicKey: publicKey,
status: status,
message: message,
id: id,
);
}
abstract class ReplaceableJob extends ClientJob {
ReplaceableJob({
required super.title,
super.id,
super.status,
super.message,
});
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) => false;
}
class ChangeAutoUpgradeSettingsJob extends ReplaceableJob {
ChangeAutoUpgradeSettingsJob({
required this.enable,
required this.allowReboot,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.change_auto_upgrade_settings'.tr());
final bool enable;
final bool allowReboot;
@override
Future<(bool, String)> execute() async => getIt<ApiConnectionRepository>()
.setAutoUpgradeSettings(enable, allowReboot);
@override
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) {
final currentSettings = getIt<ApiConnectionRepository>()
.apiData
.settings
.data
?.autoUpgradeSettings;
if (currentSettings == null) {
return false;
}
return currentSettings.enable == enable &&
currentSettings.allowReboot == allowReboot;
}
@override
List<Object> get props => [id, title, user, publicKey];
List<Object> get props => [...super.props, enable, allowReboot];
@override
ChangeAutoUpgradeSettingsJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ChangeAutoUpgradeSettingsJob(
enable: enable,
allowReboot: allowReboot,
status: status,
message: message,
id: id,
);
}
class ChangeServerTimezoneJob extends ReplaceableJob {
ChangeServerTimezoneJob({
required this.timezone,
super.status,
super.message,
super.id,
}) : super(title: 'jobs.change_server_timezone'.tr());
final String timezone;
@override
Future<(bool, String)> execute() async =>
getIt<ApiConnectionRepository>().setServerTimezone(timezone);
@override
bool shouldRemoveInsteadOfAdd(final List<ClientJob> jobs) {
final currentSettings =
getIt<ApiConnectionRepository>().apiData.settings.data?.timezone;
if (currentSettings == null) {
return false;
}
return currentSettings == timezone;
}
@override
List<Object> get props => [...super.props, timezone];
@override
ChangeServerTimezoneJob copyWithNewStatus({
required final JobStatusEnum status,
final String? message,
}) =>
ChangeServerTimezoneJob(
timezone: timezone,
status: status,
message: message,
id: id,
);
}

View File

@ -1,13 +1,14 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_api.graphql.dart';
part 'api_token.g.dart';
@JsonSerializable()
class ApiToken {
class ApiToken extends Equatable {
factory ApiToken.fromJson(final Map<String, dynamic> json) =>
_$ApiTokenFromJson(json);
ApiToken({
const ApiToken({
required this.name,
required this.date,
required this.isCaller,
@ -25,4 +26,7 @@ class ApiToken {
final DateTime date;
@JsonKey(name: 'is_caller')
final bool isCaller;
@override
List<Object?> get props => [name, date, isCaller];
}

View File

@ -1,12 +1,13 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'server_disk_volume.g.dart';
@JsonSerializable()
class ServerDiskVolume {
class ServerDiskVolume extends Equatable {
factory ServerDiskVolume.fromJson(final Map<String, dynamic> json) =>
_$ServerDiskVolumeFromJson(json);
ServerDiskVolume({
const ServerDiskVolume({
required this.freeSpace,
required this.model,
required this.name,
@ -25,4 +26,16 @@ class ServerDiskVolume {
final String totalSpace;
final String type;
final String usedSpace;
@override
List<Object?> get props => [
freeSpace,
model,
name,
root,
serial,
totalSpace,
type,
usedSpace,
];
}

View File

@ -1,13 +1,14 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_api.graphql.dart';
part 'server_job.g.dart';
@JsonSerializable()
class ServerJob {
class ServerJob extends Equatable {
factory ServerJob.fromJson(final Map<String, dynamic> json) =>
_$ServerJobFromJson(json);
ServerJob({
const ServerJob({
required this.name,
required this.description,
required this.status,
@ -50,6 +51,22 @@ class ServerJob {
final String? result;
final String? statusText;
final DateTime? finishedAt;
@override
List<Object?> get props => [
name,
description,
status,
uid,
typeId,
updatedAt,
createdAt,
error,
progress,
result,
statusText,
finishedAt,
];
}
enum JobStatusEnum {

View File

@ -1,13 +1,14 @@
import 'dart:convert';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/services.graphql.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/json/dns_records.dart';
class Service {
class Service extends Equatable {
Service.fromGraphQL(final Query$AllServices$services$allServices service)
: this(
id: service.id,
@ -36,7 +37,7 @@ class Service {
[],
url: service.url,
);
Service({
const Service({
required this.id,
required this.displayName,
required this.description,
@ -71,7 +72,7 @@ class Service {
return '';
}
static Service empty = Service(
static Service empty = const Service(
id: 'empty',
displayName: '',
description: '',
@ -82,7 +83,7 @@ class Service {
backupDescription: '',
status: ServiceStatus.off,
storageUsage: ServiceStorageUsage(
used: const DiskSize(byte: 0),
used: DiskSize(byte: 0),
volume: '',
),
svgIcon: '',
@ -103,16 +104,36 @@ class Service {
final String svgIcon;
final String? url;
final List<DnsRecord> dnsRecords;
@override
List<Object?> get props => [
id,
displayName,
description,
isEnabled,
isRequired,
isMovable,
canBeBackedUp,
backupDescription,
status,
storageUsage,
svgIcon,
dnsRecords,
url,
];
}
class ServiceStorageUsage {
ServiceStorageUsage({
class ServiceStorageUsage extends Equatable {
const ServiceStorageUsage({
required this.used,
required this.volume,
});
final DiskSize used;
final String? volume;
@override
List<Object?> get props => [used, volume];
}
enum ServiceStatus {

View File

@ -336,7 +336,7 @@ class DigitalOceanServerProvider extends ServerProvider {
}
final volumes = await getVolumes();
final ServerVolume volumeToRemove;
final ServerProviderVolume volumeToRemove;
volumeToRemove = volumes.data.firstWhere(
(final el) => el.serverId == foundServer!.id,
);
@ -548,10 +548,10 @@ class DigitalOceanServerProvider extends ServerProvider {
);
@override
Future<GenericResult<List<ServerVolume>>> getVolumes({
Future<GenericResult<List<ServerProviderVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> volumes = [];
final List<ServerProviderVolume> volumes = [];
final result = await _adapter.api().getVolumes();
@ -568,7 +568,7 @@ class DigitalOceanServerProvider extends ServerProvider {
int id = 0;
for (final rawVolume in result.data) {
final String volumeName = rawVolume.name;
final volume = ServerVolume(
final volume = ServerProviderVolume(
id: id++,
name: volumeName,
sizeByte: rawVolume.sizeGigabytes * 1024 * 1024 * 1024,
@ -597,8 +597,10 @@ class DigitalOceanServerProvider extends ServerProvider {
}
@override
Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
ServerVolume? volume;
Future<GenericResult<ServerProviderVolume?>> createVolume(
final int gb,
) async {
ServerProviderVolume? volume;
final result = await _adapter.api().createVolume(gb);
@ -623,7 +625,7 @@ class DigitalOceanServerProvider extends ServerProvider {
}
final String volumeName = result.data!.name;
volume = ServerVolume(
volume = ServerProviderVolume(
id: getVolumesResult.data.length,
name: volumeName,
sizeByte: result.data!.sizeGigabytes,
@ -638,10 +640,10 @@ class DigitalOceanServerProvider extends ServerProvider {
);
}
Future<GenericResult<ServerVolume?>> getVolume(
Future<GenericResult<ServerProviderVolume?>> getVolume(
final String volumeUuid,
) async {
ServerVolume? requestedVolume;
ServerProviderVolume? requestedVolume;
final result = await getVolumes();
@ -668,7 +670,7 @@ class DigitalOceanServerProvider extends ServerProvider {
@override
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
@ -678,7 +680,7 @@ class DigitalOceanServerProvider extends ServerProvider {
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
) async =>
_adapter.api().detachVolume(
volume.name,
@ -687,7 +689,7 @@ class DigitalOceanServerProvider extends ServerProvider {
@override
Future<GenericResult<void>> deleteVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
) async =>
_adapter.api().deleteVolume(
volume.uuid!,
@ -695,7 +697,7 @@ class DigitalOceanServerProvider extends ServerProvider {
@override
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
final DiskSize size,
) async =>
_adapter.api().resizeVolume(

View File

@ -280,7 +280,7 @@ class HetznerServerProvider extends ServerProvider {
id: serverResult.data!.id,
ip4: serverResult.data!.publicNet.ipv4!.ip,
createTime: DateTime.now(),
volume: ServerVolume(
volume: ServerProviderVolume(
id: volume.id,
name: volume.name,
sizeByte: volume.size * 1024 * 1024 * 1024,
@ -580,10 +580,10 @@ class HetznerServerProvider extends ServerProvider {
}
@override
Future<GenericResult<List<ServerVolume>>> getVolumes({
Future<GenericResult<List<ServerProviderVolume>>> getVolumes({
final String? status,
}) async {
final List<ServerVolume> volumes = [];
final List<ServerProviderVolume> volumes = [];
final result = await _adapter.api().getVolumes();
@ -603,7 +603,7 @@ class HetznerServerProvider extends ServerProvider {
final volumeServer = rawVolume.serverId;
final String volumeName = rawVolume.name;
final volumeDevice = rawVolume.linuxDevice;
final volume = ServerVolume(
final volume = ServerProviderVolume(
id: volumeId,
name: volumeName,
sizeByte: volumeSize,
@ -629,8 +629,10 @@ class HetznerServerProvider extends ServerProvider {
}
@override
Future<GenericResult<ServerVolume?>> createVolume(final int gb) async {
ServerVolume? volume;
Future<GenericResult<ServerProviderVolume?>> createVolume(
final int gb,
) async {
ServerProviderVolume? volume;
final result = await _adapter.api().createVolume(gb);
@ -644,7 +646,7 @@ class HetznerServerProvider extends ServerProvider {
}
try {
volume = ServerVolume(
volume = ServerProviderVolume(
id: result.data!.id,
name: result.data!.name,
sizeByte: result.data!.size * 1024 * 1024 * 1024,
@ -669,12 +671,14 @@ class HetznerServerProvider extends ServerProvider {
}
@override
Future<GenericResult<void>> deleteVolume(final ServerVolume volume) async =>
Future<GenericResult<void>> deleteVolume(
final ServerProviderVolume volume,
) async =>
_adapter.api().deleteVolume(volume.id);
@override
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
final DiskSize size,
) async =>
_adapter.api().resizeVolume(
@ -690,7 +694,7 @@ class HetznerServerProvider extends ServerProvider {
@override
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
final int serverId,
) async =>
_adapter.api().attachVolume(
@ -706,7 +710,7 @@ class HetznerServerProvider extends ServerProvider {
@override
Future<GenericResult<bool>> detachVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
) async =>
_adapter.api().detachVolume(
volume.id,

View File

@ -94,35 +94,37 @@ abstract class ServerProvider {
/// main server type pricing
Future<GenericResult<AdditionalPricing?>> getAdditionalPricing();
/// Returns [ServerVolume] of all available volumes
/// Returns [ServerProviderVolume] of all available volumes
/// assigned to the authorized user and attached to active machine.
Future<GenericResult<List<ServerVolume>>> getVolumes({final String? status});
Future<GenericResult<List<ServerProviderVolume>>> getVolumes({
final String? status,
});
/// Tries to create an empty unattached [ServerVolume].
/// Tries to create an empty unattached [ServerProviderVolume].
///
/// If success, returns this volume information.
Future<GenericResult<ServerVolume?>> createVolume(final int gb);
Future<GenericResult<ServerProviderVolume?>> createVolume(final int gb);
/// Tries to delete the requested accessible [ServerVolume].
Future<GenericResult<void>> deleteVolume(final ServerVolume volume);
/// Tries to delete the requested accessible [ServerProviderVolume].
Future<GenericResult<void>> deleteVolume(final ServerProviderVolume volume);
/// Tries to resize the requested accessible [ServerVolume]
/// Tries to resize the requested accessible [ServerProviderVolume]
/// to the provided size **(not by!)**, must be greater than current size.
Future<GenericResult<bool>> resizeVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
final DiskSize size,
);
/// Tries to attach the requested accessible [ServerVolume]
/// Tries to attach the requested accessible [ServerProviderVolume]
/// to an accessible machine by the provided identificator.
Future<GenericResult<bool>> attachVolume(
final ServerVolume volume,
final ServerProviderVolume volume,
final int serverId,
);
/// Tries to attach the requested accessible [ServerVolume]
/// Tries to attach the requested accessible [ServerProviderVolume]
/// from any machine.
Future<GenericResult<bool>> detachVolume(final ServerVolume volume);
Future<GenericResult<bool>> detachVolume(final ServerProviderVolume volume);
/// Returns metedata of an accessible machine by the provided identificator
/// to show on ServerDetailsScreen.

View File

@ -1,12 +1,12 @@
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/jobs_content/server_job_card.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
@ -19,13 +19,39 @@ class JobsContent extends StatelessWidget {
final ScrollController controller;
IconData _getIcon(final JobStatusEnum status) {
switch (status) {
case JobStatusEnum.created:
return Icons.query_builder_outlined;
case JobStatusEnum.running:
return Icons.pending_outlined;
case JobStatusEnum.finished:
return Icons.check_circle_outline;
case JobStatusEnum.error:
return Icons.error_outline;
}
}
Color _getColor(final JobStatusEnum status, final BuildContext context) {
switch (status) {
case JobStatusEnum.created:
return Theme.of(context).colorScheme.secondary;
case JobStatusEnum.running:
return Theme.of(context).colorScheme.tertiary;
case JobStatusEnum.finished:
return Theme.of(context).colorScheme.primary;
case JobStatusEnum.error:
return Theme.of(context).colorScheme.error;
}
}
@override
Widget build(final BuildContext context) {
final List<ServerJob> serverJobs =
context.watch<ServerJobsCubit>().state.serverJobList;
context.watch<ServerJobsBloc>().state.serverJobList;
final bool hasRemovableJobs =
context.watch<ServerJobsCubit>().state.hasRemovableJobs;
context.watch<ServerJobsBloc>().state.hasRemovableJobs;
return BlocBuilder<JobsCubit, JobsState>(
builder: (final context, final state) {
@ -68,8 +94,274 @@ class JobsContent extends StatelessWidget {
}
} else if (state is JobsStateLoading) {
widgets = [
const SizedBox(height: 80),
BrandLoader.horizontal(),
...state.clientJobList.map(
(final j) => Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(j.status),
color: _getColor(j.status, context),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
j.title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (j.message != null)
Text(
j.message!,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
),
),
if (state.rebuildRequired)
Builder(
builder: (final context) {
final rebuildJob = serverJobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(rebuildJob?.status ?? JobStatusEnum.created),
color: _getColor(
rebuildJob?.status ?? JobStatusEnum.created,
context,
),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rebuildJob?.name ??
'jobs.rebuild_system'.tr(),
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (rebuildJob?.description != null)
Text(
rebuildJob!.description,
style:
Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: rebuildJob?.progress == null
? 0.0
: ((rebuildJob!.progress ?? 0) < 1)
? null
: rebuildJob.progress! / 100.0,
color: _getColor(
rebuildJob?.status ?? JobStatusEnum.created,
context,
),
backgroundColor: Theme.of(context)
.colorScheme
.surfaceVariant,
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
if (rebuildJob?.error != null ||
rebuildJob?.result != null ||
rebuildJob?.statusText != null)
Text(
rebuildJob?.error ??
rebuildJob?.result ??
rebuildJob?.statusText ??
'',
style:
Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
);
},
),
];
} else if (state is JobsStateFinished) {
widgets = [
...state.clientJobList.map(
(final j) => Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(j.status),
color: _getColor(j.status, context),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
j.title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (j.message != null)
Text(
j.message!,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
),
),
if (state.rebuildRequired)
Builder(
builder: (final context) {
final rebuildJob = serverJobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null) {
return const SizedBox();
}
return Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
_getIcon(rebuildJob.status),
color: _getColor(
rebuildJob.status,
context,
),
),
),
Expanded(
child: Card(
color: Theme.of(context).colorScheme.surfaceVariant,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 10,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rebuildJob.name,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
Text(
rebuildJob.description,
style: Theme.of(context).textTheme.labelSmall,
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: rebuildJob.progress == null
? 0.0
: ((rebuildJob.progress ?? 0) < 1)
? null
: rebuildJob.progress! / 100.0,
color: _getColor(
rebuildJob.status,
context,
),
backgroundColor: Theme.of(context)
.colorScheme
.surfaceVariant,
minHeight: 7.0,
borderRadius: BorderRadius.circular(7.0),
),
const SizedBox(height: 8),
if (rebuildJob.error != null ||
rebuildJob.result != null ||
rebuildJob.statusText != null)
Text(
rebuildJob.error ??
rebuildJob.result ??
rebuildJob.statusText ??
'',
style:
Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
],
);
},
),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().acknowledgeFinished(),
text: 'basis.done'.tr(),
),
];
} else if (state is JobsStateWithJobs) {
widgets = [
@ -84,19 +376,31 @@ class JobsContent extends StatelessWidget {
horizontal: 15,
vertical: 10,
),
child: Text(
j.title,
style:
Theme.of(context).textTheme.labelLarge?.copyWith(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
j.title,
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
if (j.message != null)
Text(
j.message!,
style: Theme.of(context).textTheme.labelSmall,
),
],
),
),
),
),
const SizedBox(width: 10),
const SizedBox(width: 8),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
@ -116,7 +420,7 @@ class JobsContent extends StatelessWidget {
],
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
BrandButton.rised(
onPressed: () => context.read<JobsCubit>().applyAll(),
text: 'jobs.start'.tr(),
@ -152,8 +456,8 @@ class JobsContent extends StatelessWidget {
IconButton(
onPressed: hasRemovableJobs
? () => context
.read<ServerJobsCubit>()
.removeAllFinishedJobs()
.read<ServerJobsBloc>()
.add(RemoveAllFinishedJobs())
: null,
icon: const Icon(Icons.clear_all),
color: Theme.of(context).colorScheme.onBackground,
@ -161,21 +465,25 @@ class JobsContent extends StatelessWidget {
],
),
),
...serverJobs.map(
(final job) => Dismissible(
key: ValueKey(job.uid),
direction: job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error
? DismissDirection.horizontal
: DismissDirection.none,
child: ServerJobCard(
serverJob: job,
...serverJobs
.whereNot((final job) => job.uid == state.rebuildJobUid)
.map(
(final job) => Dismissible(
key: ValueKey(job.uid),
direction: job.status == JobStatusEnum.finished ||
job.status == JobStatusEnum.error
? DismissDirection.horizontal
: DismissDirection.none,
child: ServerJobCard(
serverJob: job,
),
onDismissed: (final direction) {
context.read<ServerJobsBloc>().add(
RemoveServerJob(job.uid),
);
},
),
),
onDismissed: (final direction) {
context.read<ServerJobsCubit>().removeServerJob(job.uid);
},
),
),
const SizedBox(height: 24),
],
);

View File

@ -2,10 +2,10 @@ 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/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.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';
@ -31,19 +31,18 @@ class BackupDetailsPage extends StatelessWidget {
Widget build(final BuildContext context) {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
final BackupsState backupsState = context.watch<BackupsCubit>().state;
final BackupsState backupsState = context.watch<BackupsBloc>().state;
final bool isBackupInitialized = backupsState.isInitialized;
final StateType providerState = isReady && isBackupInitialized
? StateType.stable
: StateType.uninitialized;
final bool preventActions = backupsState.preventActions;
final List<Backup> backups = backupsState.backups;
final bool refreshing = backupsState.refreshing;
final List<Service> services =
context.watch<ServicesCubit>().state.servicesThatCanBeBackedUp;
context.watch<ServicesBloc>().state.servicesThatCanBeBackedUp;
final Duration? autobackupPeriod = backupsState.autobackupPeriod;
final List<ServerJob> backupJobs = context
.watch<ServerJobsCubit>()
.watch<ServerJobsBloc>()
.state
.backupJobList
.where((final job) => job.status != JobStatusEnum.finished)
@ -75,8 +74,10 @@ class BackupDetailsPage extends StatelessWidget {
BrandButton.rised(
onPressed: preventActions
? null
: () async {
await context.read<BackupsCubit>().initializeBackups();
: () {
context
.read<BackupsBloc>()
.add(const InitializeBackupsRepository());
},
text: 'backup.initialize'.tr(),
),
@ -297,7 +298,7 @@ class BackupDetailsPage extends StatelessWidget {
children: backups.take(15).map(
(final Backup backup) {
final service = context
.read<ServicesCubit>()
.read<ServicesBloc>()
.state
.getServiceById(backup.serviceId);
return ListTile(
@ -334,11 +335,12 @@ class BackupDetailsPage extends StatelessWidget {
'backup.forget_snapshot_alert'.tr(),
actionButtonTitle:
'backup.forget_snapshot'.tr(),
actionButtonOnPressed: () => {
context.read<BackupsCubit>().forgetSnapshot(
backup.id,
),
},
actionButtonOnPressed: () =>
context.read<BackupsBloc>().add(
ForgetSnapshot(
backup.id,
),
),
);
},
title: Text(
@ -391,18 +393,6 @@ class BackupDetailsPage extends StatelessWidget {
const SizedBox(height: 8),
const Divider(),
const SizedBox(height: 8),
ListTile(
title: Text(
'backup.refresh'.tr(),
),
onTap: refreshing
? null
: () => {context.read<BackupsCubit>().updateBackups()},
enabled: !refreshing,
leading: const Icon(
Icons.refresh_outlined,
),
),
if (providerState != StateType.uninitialized)
Column(
children: [
@ -425,32 +415,11 @@ class BackupDetailsPage extends StatelessWidget {
),
onTap: preventActions
? null
: () => {context.read<BackupsCubit>().forceUpdateBackups()},
),
const SizedBox(height: 8),
const Divider(),
const SizedBox(height: 8),
ListTile(
title: Text(
'backup.reupload_key'.tr(),
style: TextStyle(
color: overrideColor,
),
),
subtitle: Text(
'backup.reupload_key_subtitle'.tr(),
style: TextStyle(
color: overrideColor,
),
),
leading: Icon(
Icons.warning_amber_outlined,
color: overrideColor,
),
onTap: preventActions
? null
: () => {context.read<BackupsCubit>().reuploadKey()},
: () => context
.read<BackupsBloc>()
.add(const ForceSnapshotListUpdate()),
),
// TODO: Return reupload key button in some form
],
),
],

View File

@ -3,8 +3,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/service.dart';
import 'package:selfprivacy/ui/helpers/modals.dart';
@ -25,10 +25,10 @@ class BackupsListPage extends StatelessWidget {
// If the service is null, get all backups from state. If not null, call the
// serviceBackups(serviceId) on the backups state.
final List<Backup> backups = service == null
? context.watch<BackupsCubit>().state.backups
: context.watch<BackupsCubit>().state.serviceBackups(service!.id);
? context.watch<BackupsBloc>().state.backups
: context.watch<BackupsBloc>().state.serviceBackups(service!.id);
final bool preventActions =
context.watch<BackupsCubit>().state.preventActions;
context.watch<BackupsBloc>().state.preventActions;
return BrandHeroScreen(
heroTitle: 'backup.snapshots_title'.tr(),
hasFlashButton: true,
@ -43,7 +43,7 @@ class BackupsListPage extends StatelessWidget {
...backups.map(
(final Backup backup) {
final service = context
.read<ServicesCubit>()
.read<ServicesBloc>()
.state
.getServiceById(backup.serviceId);
return ListTile(
@ -75,11 +75,9 @@ class BackupsListPage extends StatelessWidget {
alertTitle: 'backup.forget_snapshot'.tr(),
description: 'backup.forget_snapshot_alert'.tr(),
actionButtonTitle: 'backup.forget_snapshot'.tr(),
actionButtonOnPressed: () => {
context.read<BackupsCubit>().forgetSnapshot(
backup.id,
),
},
actionButtonOnPressed: () => context
.read<BackupsBloc>()
.add(ForgetSnapshot(backup.id)),
);
},
title: Text(

View File

@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/utils/extensions/duration.dart';
class ChangeAutobackupsPeriodModal extends StatefulWidget {
@ -34,13 +34,13 @@ class _ChangeAutobackupsPeriodModalState
@override
void initState() {
super.initState();
selectedPeriod = context.read<BackupsCubit>().state.autobackupPeriod;
selectedPeriod = context.read<BackupsBloc>().state.autobackupPeriod;
}
@override
Widget build(final BuildContext context) {
final Duration? initialAutobackupPeriod =
context.watch<BackupsCubit>().state.autobackupPeriod;
context.watch<BackupsBloc>().state.autobackupPeriod;
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
@ -91,8 +91,8 @@ class _ChangeAutobackupsPeriodModalState
? null
: () {
context
.read<BackupsCubit>()
.setAutobackupPeriod(selectedPeriod);
.read<BackupsBloc>()
.add(SetAutobackupPeriod(selectedPeriod));
Navigator.of(context).pop();
},
child: Text(

View File

@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
class ChangeRotationQuotasModal extends StatefulWidget {
@ -27,7 +27,7 @@ enum QuotaUnits {
}
class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
AutobackupQuotas selectedQuotas = AutobackupQuotas(
AutobackupQuotas selectedQuotas = const AutobackupQuotas(
last: 3,
daily: 7,
weekly: 4,
@ -40,7 +40,7 @@ class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
void initState() {
super.initState();
selectedQuotas =
context.read<BackupsCubit>().state.autobackupQuotas ?? selectedQuotas;
context.read<BackupsBloc>().state.autobackupQuotas ?? selectedQuotas;
}
String generateSubtitle(final int value, final QuotaUnits unit) {
@ -83,7 +83,7 @@ class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
@override
Widget build(final BuildContext context) {
final AutobackupQuotas? initialAutobackupQuotas =
context.watch<BackupsCubit>().state.autobackupQuotas;
context.watch<BackupsBloc>().state.autobackupQuotas;
return ListView(
controller: widget.scrollController,
padding: const EdgeInsets.all(16),
@ -190,8 +190,8 @@ class _ChangeRotationQuotasModalState extends State<ChangeRotationQuotasModal> {
? null
: () {
context
.read<BackupsCubit>()
.setAutobackupQuotas(selectedQuotas);
.read<BackupsBloc>()
.add(SetAutobackupQuotas(selectedQuotas));
Navigator.of(context).pop();
},
child: Text(

View File

@ -2,9 +2,9 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
import 'package:selfprivacy/utils/platform_adapter.dart';
@ -34,7 +34,7 @@ class _CopyEncryptionKeyModalState extends State<CopyEncryptionKeyModal> {
@override
Widget build(final BuildContext context) {
final String? encryptionKey =
context.watch<BackupsCubit>().state.backblazeBucket?.encryptionKey;
context.watch<BackupsBloc>().state.backblazeBucket?.encryptionKey;
if (encryptionKey == null) {
return ListView(
controller: widget.scrollController,

View File

@ -1,8 +1,8 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -29,7 +29,7 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
void initState() {
super.initState();
final List<String> busyServices = context
.read<ServerJobsCubit>()
.read<ServerJobsBloc>()
.state
.backupJobList
.where(
@ -48,7 +48,7 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
@override
Widget build(final BuildContext context) {
final List<String> busyServices = context
.watch<ServerJobsCubit>()
.watch<ServerJobsBloc>()
.state
.backupJobList
.where(
@ -147,8 +147,8 @@ class _CreateBackupsModalState extends State<CreateBackupsModal> {
? null
: () {
context
.read<BackupsCubit>()
.createMultipleBackups(selectedServices);
.read<BackupsBloc>()
.add(CreateBackups(selectedServices));
Navigator.of(context).pop();
},
child: Text(

View File

@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.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/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -34,7 +34,7 @@ class _SnapshotModalState extends State<SnapshotModal> {
@override
Widget build(final BuildContext context) {
final List<String> busyServices = context
.watch<ServerJobsCubit>()
.watch<ServerJobsBloc>()
.state
.backupJobList
.where(
@ -48,7 +48,7 @@ class _SnapshotModalState extends State<SnapshotModal> {
final bool isServiceBusy = busyServices.contains(widget.snapshot.serviceId);
final Service? service = context
.read<ServicesCubit>()
.read<ServicesBloc>()
.state
.getServiceById(widget.snapshot.serviceId);
@ -153,9 +153,11 @@ class _SnapshotModalState extends State<SnapshotModal> {
onPressed: isServiceBusy
? null
: () {
context.read<BackupsCubit>().restoreBackup(
widget.snapshot.id,
selectedStrategy,
context.read<BackupsBloc>().add(
RestoreBackup(
widget.snapshot.id,
selectedStrategy,
),
);
Navigator.of(context).pop();
getIt<NavigationService>()

View File

@ -2,8 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/models/json/api_token.dart';
import 'package:selfprivacy/ui/components/info_box/info_box.dart';
@ -22,12 +21,11 @@ class DevicesScreen extends StatefulWidget {
class _DevicesScreenState extends State<DevicesScreen> {
@override
Widget build(final BuildContext context) {
final ApiDevicesState devicesStatus =
context.watch<ApiDevicesCubit>().state;
final DevicesState devicesStatus = context.watch<DevicesBloc>().state;
return RefreshIndicator(
onRefresh: () async {
await context.read<ApiDevicesCubit>().refresh();
await context.read<DevicesBloc>().refresh();
},
child: BrandHeroScreen(
heroTitle: 'devices.main_screen.header'.tr(),
@ -35,13 +33,13 @@ class _DevicesScreenState extends State<DevicesScreen> {
hasBackButton: true,
hasFlashButton: false,
children: [
if (devicesStatus.status == LoadingStatus.uninitialized) ...[
if (devicesStatus is DevicesInitial) ...[
const Center(
heightFactor: 8,
child: CircularProgressIndicator(),
),
],
if (devicesStatus.status != LoadingStatus.uninitialized) ...[
if (devicesStatus is! DevicesInitial) ...[
_DevicesInfo(
devicesStatus: devicesStatus,
),
@ -70,7 +68,7 @@ class _DevicesInfo extends StatelessWidget {
required this.devicesStatus,
});
final ApiDevicesState devicesStatus;
final DevicesState devicesStatus;
@override
Widget build(final BuildContext context) => Column(
@ -82,7 +80,9 @@ class _DevicesInfo extends StatelessWidget {
color: Theme.of(context).colorScheme.secondary,
),
),
_DeviceTile(device: devicesStatus.thisDevice),
_DeviceTile(
device: devicesStatus.thisDevice,
),
const Divider(height: 1),
const SizedBox(height: 16),
Text(
@ -91,14 +91,18 @@ class _DevicesInfo extends StatelessWidget {
color: Theme.of(context).colorScheme.secondary,
),
),
if (devicesStatus.status == LoadingStatus.refreshing) ...[
if (devicesStatus is DevicesDeleting) ...[
const Center(
heightFactor: 4,
child: CircularProgressIndicator(),
),
],
...devicesStatus.otherDevices
.map((final device) => _DeviceTile(device: device)),
if (devicesStatus is! DevicesDeleting)
...devicesStatus.otherDevices.map(
(final device) => _DeviceTile(
device: device,
),
),
],
);
}
@ -110,7 +114,7 @@ class _DeviceTile extends StatelessWidget {
@override
Widget build(final BuildContext context) => ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
contentPadding: EdgeInsets.zero,
title: Text(device.name),
subtitle: Text(
'devices.main_screen.access_granted_on'
@ -161,7 +165,7 @@ class _DeviceTile extends StatelessWidget {
TextButton(
child: Text('devices.revoke_device_alert.yes'.tr()),
onPressed: () {
context.read<ApiDevicesCubit>().deleteDevice(device);
context.read<DevicesBloc>().add(DeleteDevice(device));
Navigator.of(context).pop();
},
),

View File

@ -1,7 +1,7 @@
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
@ -17,7 +17,7 @@ class NewDeviceScreen extends StatelessWidget {
hasFlashButton: false,
children: [
FutureBuilder(
future: context.read<ApiDevicesCubit>().getNewDeviceKey(),
future: context.read<DevicesBloc>().getNewDeviceKey(),
builder: (
final BuildContext context,
final AsyncSnapshot<Object?> snapshot,

View File

@ -1,10 +1,9 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/tls_options.dart';
import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart';
import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
@RoutePage()
@ -89,15 +88,11 @@ class _DeveloperSettingsPageState extends State<DeveloperSettingsPage> {
),
),
ListTile(
title: const Text('ApiDevicesCubit'),
title: const Text('ApiConnectionRepository status'),
subtitle: Text(
context.watch<ApiDevicesCubit>().state.status.toString(),
),
),
ListTile(
title: const Text('RecoveryKeyCubit'),
subtitle: Text(
context.watch<RecoveryKeyCubit>().state.loadingStatus.toString(),
getIt<ApiConnectionRepository>()
.currentConnectionStatus
.toString(),
),
),
],

View File

@ -4,8 +4,6 @@ import 'package:flutter/material.dart';
import 'package:ionicons/ionicons.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
@ -21,9 +19,6 @@ class MorePage extends StatelessWidget {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
final bool? usesBinds =
context.watch<ApiServerVolumeCubit>().state.usesBinds;
return Scaffold(
appBar: Breakpoints.small.isActive(context)
? PreferredSize(
@ -39,31 +34,6 @@ class MorePage extends StatelessWidget {
padding: paddingH15V0,
child: Column(
children: [
if (isReady && usesBinds != null && !usesBinds)
_MoreMenuItem(
title: 'storage.start_migration_button'.tr(),
iconData: Icons.drive_file_move_outline,
goTo: () => ServicesMigrationRoute(
diskStatus:
context.read<ApiServerVolumeCubit>().state.diskStatus,
services: context
.read<ServicesCubit>()
.state
.services
.where(
(final service) =>
service.id == 'bitwarden' ||
service.id == 'gitea' ||
service.id == 'pleroma' ||
service.id == 'mailserver' ||
service.id == 'nextcloud',
)
.toList(),
isMigration: true,
),
subtitle: 'storage.data_migration_notice'.tr(),
accent: true,
),
if (!isReady)
_MoreMenuItem(
title: 'more_page.configuration_wizard'.tr(),

View File

@ -2,11 +2,11 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/config/brand_theme.dart';
import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart';
import 'package:selfprivacy/logic/bloc/backups/backups_bloc.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart';
import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/models/state_types.dart';
import 'package:selfprivacy/ui/components/brand_header/brand_header.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart';
@ -30,11 +30,11 @@ class _ProvidersPageState extends State<ProvidersPage> {
final bool isReady = context.watch<ServerInstallationCubit>().state
is ServerInstallationFinished;
final bool isBackupInitialized =
context.watch<BackupsCubit>().state.isInitialized;
context.watch<BackupsBloc>().state.isInitialized;
final DnsRecordsStatus dnsStatus =
context.watch<DnsRecordsCubit>().state.dnsState;
final diskStatus = context.watch<ApiServerVolumeCubit>().state.diskStatus;
final diskStatus = context.watch<VolumesBloc>().state.diskStatus;
final ServerInstallationState appConfig =
context.watch<ServerInstallationCubit>().state;

View File

@ -3,8 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart';
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/components/cards/filled_card.dart';
@ -21,34 +20,28 @@ class RecoveryKeyPage extends StatefulWidget {
}
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
@override
void initState() {
super.initState();
context.read<RecoveryKeyCubit>().load();
}
@override
Widget build(final BuildContext context) {
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state;
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyBloc>().state;
final List<Widget> widgets;
String? subtitle =
keyStatus.exists ? null : 'recovery_key.key_main_description'.tr();
switch (keyStatus.loadingStatus) {
case LoadingStatus.refreshing:
switch (keyStatus) {
case RecoveryKeyRefreshing():
subtitle = 'recovery_key.key_synchronizing'.tr();
widgets = [
const Center(child: CircularProgressIndicator()),
];
break;
case LoadingStatus.success:
case RecoveryKeyLoaded():
widgets = [
const RecoveryKeyContent(),
];
break;
case LoadingStatus.uninitialized:
case LoadingStatus.error:
case RecoveryKeyInitial():
case RecoveryKeyError():
subtitle = 'recovery_key.key_connection_error'.tr();
widgets = [
const Icon(Icons.sentiment_dissatisfied_outlined),
@ -58,7 +51,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
return RefreshIndicator(
onRefresh: () async {
context.read<RecoveryKeyCubit>().load();
context.read<RecoveryKeyBloc>().add(const RecoveryKeyStatusRefresh());
},
child: BrandHeroScreen(
heroTitle: 'recovery_key.key_main_header'.tr(),
@ -83,7 +76,7 @@ class _RecoveryKeyContentState extends State<RecoveryKeyContent> {
@override
Widget build(final BuildContext context) {
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyCubit>().state;
final RecoveryKeyState keyStatus = context.watch<RecoveryKeyBloc>().state;
return Column(
children: [
@ -243,7 +236,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
});
try {
final String token =
await context.read<RecoveryKeyCubit>().generateRecoveryKey(
await context.read<RecoveryKeyBloc>().generateRecoveryKey(
numberOfUses: _isAmountToggled
? int.tryParse(_amountController.text)
: null,
@ -257,7 +250,7 @@ class _RecoveryKeyConfigurationState extends State<RecoveryKeyConfiguration> {
});
await Navigator.of(context).push(
materialRoute(
RecoveryKeyReceiving(recoveryKey: token), // TO DO
RecoveryKeyReceiving(recoveryKey: token),
),
);
} on GenerationError catch (e) {

View File

@ -13,7 +13,7 @@ class RecoveryKeyReceiving extends StatelessWidget {
Widget build(final BuildContext context) => BrandHeroScreen(
heroTitle: 'recovery_key.key_main_header'.tr(),
heroSubtitle: 'recovery_key.key_receiving_description'.tr(),
hasBackButton: true,
hasBackButton: false,
hasFlashButton: false,
children: [
const Divider(),

View File

@ -2,13 +2,12 @@ import 'package:auto_route/auto_route.dart';
import 'package:cubit_form/cubit_form.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
import 'package:selfprivacy/logic/common_enum/common_enum.dart';
import 'package:selfprivacy/logic/cubit/client_jobs/client_jobs_cubit.dart';
import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart';
import 'package:selfprivacy/logic/cubit/server_volumes/server_volume_cubit.dart';
import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart';
import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart';
@ -81,7 +80,7 @@ class _ServerDetailsScreenState extends State<ServerDetailsScreen>
heroSubtitle: 'server.description'.tr(),
children: [
StorageCard(
diskStatus: context.watch<ApiServerVolumeCubit>().state.diskStatus,
diskStatus: context.watch<VolumesBloc>().state.diskStatus,
),
const SizedBox(height: 16),
const _ServerSettings(),

View File

@ -30,24 +30,32 @@ class _ServerSettingsState extends State<_ServerSettings> {
value: allowAutoUpgrade ?? false,
onChanged: (final switched) {
context.read<JobsCubit>().addJob(
RebuildServerJob(title: 'jobs.upgrade_server'.tr()),
);
context
.read<ServerDetailsCubit>()
.repository
.setAutoUpgradeSettings(
AutoUpgradeSettings(
enable: switched,
ChangeAutoUpgradeSettingsJob(
allowReboot: rebootAfterUpgrade ?? false,
enable: switched,
),
);
setState(() {
allowAutoUpgrade = switched;
});
},
title: Text('server.allow_autoupgrade'.tr()),
title: Text(
'server.allow_autoupgrade'.tr(),
style: TextStyle(
fontStyle: allowAutoUpgrade !=
serverDetailsState.autoUpgradeSettings.enable
? FontStyle.italic
: FontStyle.normal,
),
),
subtitle: Text(
'server.allow_autoupgrade_hint'.tr(),
style: TextStyle(
fontStyle: allowAutoUpgrade !=
serverDetailsState.autoUpgradeSettings.enable
? FontStyle.italic
: FontStyle.normal,
),
),
activeColor: Theme.of(context).colorScheme.primary,
),
@ -55,24 +63,32 @@ class _ServerSettingsState extends State<_ServerSettings> {
value: rebootAfterUpgrade ?? false,
onChanged: (final switched) {
context.read<JobsCubit>().addJob(
RebuildServerJob(title: 'jobs.upgrade_server'.tr()),
);
context
.read<ServerDetailsCubit>()
.repository
.setAutoUpgradeSettings(
AutoUpgradeSettings(
enable: allowAutoUpgrade ?? false,
ChangeAutoUpgradeSettingsJob(
allowReboot: switched,
enable: allowAutoUpgrade ?? false,
),
);
setState(() {
rebootAfterUpgrade = switched;
});
},
title: Text('server.reboot_after_upgrade'.tr()),
title: Text(
'server.reboot_after_upgrade'.tr(),
style: TextStyle(
fontStyle: rebootAfterUpgrade !=
serverDetailsState.autoUpgradeSettings.allowReboot
? FontStyle.italic
: FontStyle.normal,
),
),
subtitle: Text(
'server.reboot_after_upgrade_hint'.tr(),
style: TextStyle(
fontStyle: rebootAfterUpgrade !=
serverDetailsState.autoUpgradeSettings.allowReboot
? FontStyle.italic
: FontStyle.normal,
),
),
activeColor: Theme.of(context).colorScheme.primary,
),
@ -82,9 +98,6 @@ class _ServerSettingsState extends State<_ServerSettings> {
serverDetailsState.serverTimezone.toString(),
),
onTap: () {
context.read<JobsCubit>().addJob(
RebuildServerJob(title: 'jobs.upgrade_server'.tr()),
);
Navigator.of(context).push(
materialRoute(
const SelectTimezone(),

View File

@ -140,8 +140,10 @@ class _SelectTimezoneState extends State<SelectTimezone> {
'GMT ${duration.toTimezoneOffsetFormat()} ${area.isNotEmpty ? '($area)' : ''}',
),
onTap: () {
context.read<ServerDetailsCubit>().repository.setTimezone(
location.name,
context.read<JobsCubit>().addJob(
ChangeServerTimezoneJob(
timezone: location.name,
),
);
Navigator.of(context).pop();
},

View File

@ -1,79 +0,0 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:selfprivacy/logic/cubit/server_jobs/server_jobs_cubit.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
import 'package:selfprivacy/ui/components/brand_linear_indicator/brand_linear_indicator.dart';
import 'package:selfprivacy/ui/components/buttons/brand_button.dart';
import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart';
import 'package:selfprivacy/ui/pages/root_route.dart';
import 'package:selfprivacy/utils/route_transitions/basic.dart';
class MigrationProcessPage extends StatefulWidget {
const MigrationProcessPage({super.key});
@override
State<MigrationProcessPage> createState() => _MigrationProcessPageState();
}
class _MigrationProcessPageState extends State<MigrationProcessPage> {
@override
void initState() {
super.initState();
}
@override
Widget build(final BuildContext context) {
ServerJob? job;
String? subtitle = '';
double value = 0.0;
List<Widget> children = [];
final serverJobsState = context.watch<ServerJobsCubit>().state;
if (serverJobsState.migrationJobUid != null) {
job = context.read<ServerJobsCubit>().getServerJobByUid(
serverJobsState.migrationJobUid!,
);
}
if (job == null) {
subtitle = 'basis.loading'.tr();
} else {
value = job.progress == null ? 0.0 : job.progress! / 100;
subtitle = job.statusText;
children = [
...children,
const SizedBox(height: 16),
if (job.finishedAt != null)
Text(
job.result!,
style: Theme.of(context).textTheme.titleMedium,
),
if (job.finishedAt != null) const SizedBox(height: 16),
if (job.finishedAt != null)
BrandButton.filled(
child: Text('storage.migration_done'.tr()),
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
materialRoute(const RootPage()),
(final predicate) => false,
);
},
),
];
}
return BrandHeroScreen(
hasBackButton: false,
heroTitle: 'storage.migration_process'.tr(),
heroSubtitle: subtitle,
children: [
BrandLinearIndicator(
value: value,
color: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
height: 4.0,
),
...children,
],
);
}
}

View File

@ -1,8 +1,8 @@
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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/bloc/server_jobs/server_jobs_bloc.dart';
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
import 'package:selfprivacy/logic/models/disk_size.dart';
import 'package:selfprivacy/logic/models/disk_status.dart';
import 'package:selfprivacy/logic/models/service.dart';
@ -18,13 +18,11 @@ class ServicesMigrationPage extends StatefulWidget {
const ServicesMigrationPage({
required this.services,
required this.diskStatus,
required this.isMigration,
super.key,
});
final DiskStatus diskStatus;
final List<Service> services;
final bool isMigration;
@override
State<ServicesMigrationPage> createState() => _ServicesMigrationPageState();
@ -171,22 +169,18 @@ class _ServicesMigrationPageState extends State<ServicesMigrationPage> {
),
),
const SizedBox(height: 16),
if (widget.isMigration || (!widget.isMigration && isVolumePicked))
if (isVolumePicked)
BrandButton.filled(
child: Text('storage.start_migration_button'.tr()),
onPressed: () {
if (widget.isMigration) {
context.read<ServerJobsCubit>().migrateToBinds(
serviceToDisk,
);
} else {
for (final service in widget.services) {
if (serviceToDisk[service.id] != null) {
context.read<ServicesCubit>().moveService(
service.id,
for (final service in widget.services) {
if (serviceToDisk[service.id] != null) {
context.read<ServicesBloc>().add(
ServiceMove(
service,
serviceToDisk[service.id]!,
);
}
),
);
}
}
context.router.popUntilRoot();

Some files were not shown because too many files have changed in this diff Show More