refactor: Introduce the API connection repository #440

Merged
inex merged 36 commits from api-connection-refactor into master 2024-02-23 16:49:39 +02:00
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 {

What the hell 😨 , looks scary. is this how it's supposed to be now for all GraphQL requests?

What the hell 😨 , looks scary. is this how it's supposed to be now for all GraphQL requests?
Review

I think there can be a better approach, but in general, yeah, every request must return a GenericResult or something like that.

I think there can be a better approach, but in general, yeah, every request must return a GenericResult or something like that.
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;

This is quite extreme, is this impossible to trigger? Do we need a notification for it?

This is quite extreme, is this impossible to trigger? Do we need a notification for it?
Review

Better safe than sorry. Events now trigger these functions, and not the direct calls.

Better safe than sorry. Events now trigger these functions, and not the direct calls.
}
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) {

This really scares me. I suspect it's in all other blocs like this... what do you think?

This really scares me. I suspect it's in all other blocs like this... what do you think?
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)

Duplication with line 62?

Duplication with line 62?
Review

There are different states emited

There are different states emited
.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>();
inex marked this conversation as resolved

imho handler registration should preceed event subscription. not sure how it works for bloc default state now, but it could easily drop event from 26 line, because handler was not yet registered.

imho handler registration should preceed event subscription. not sure how it works for bloc default state now, but it could easily drop event from 26 line, because handler was not yet registered.
_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

I am 99% sure it isn't 🙏 will do some cleaning evenetually

I am 99% sure it isn't 🙏 will do some cleaning evenetually
@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 =>

Can't we use GenericResult everywhere? Is it overkill?

Can't we use `GenericResult` everywhere? Is it overkill?
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,
),
],
),
),
),
),
],
);
},
),

it's so over

it's so over
];
} 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,

I can already see how we create tickets to refactor all that

I can already see how we create tickets to refactor all that
Review

Welp...

Server API does not return the progress value yet, but might return later.

When we pass null, it does the cute loading animation.

Welp... Server API does not return the progress value yet, but might return later. When we pass `null`, it does the cute loading animation.
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