Merge branch 'master' into move-title-in-cards

pull/449/head
NaiJi ✨ 2024-03-04 12:42:27 +02:00
commit b8b8ac43ea
159 changed files with 6876 additions and 3022 deletions

View File

@ -1,6 +1,9 @@
name: Windows Builder
on: tag
on:
push:
tags:
- '*.*.*'
jobs:
build-windows:
@ -14,7 +17,7 @@ jobs:
# Install Flutter
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.3.10'
flutter-version: '3.16.1'
channel: 'stable'
# Build Windows artifact

View File

@ -10,7 +10,7 @@ AppDir:
id: org.selfprivacy.app
name: SelfPrivacy
icon: org.selfprivacy.app
version: 0.10.0
version: 0.10.1
exec: selfprivacy
exec_args: $@
apt:

View File

@ -1,12 +0,0 @@
### Пра нас
Усё больш арганізацый жадаюць валодаць нашымі дадзенымі
Праект дазваляе толькі Вам у поўнай меры распараджацца ўласнымі **дадзенымі** на сваім сэрвэры.
### Наша місія
Лічбавая незалежнасць і прыватнасць, даступныя кожнаму
### Мэта
Распрацаваць праграму, якая дазволіць кожнаму разгарнуць свае прыватныя паслугі для сябе і сваіх суседзяў.

View File

@ -1,12 +0,0 @@
### O nás
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Naše poslání
Digitální nezávislost a soukromí dostupné všem
### Cíl
Rozvíjet program, který umožní každému nasadit své soukromé služby pro sebe a své sousedy.

View File

@ -1,12 +0,0 @@
### Über uns
Immer mehr Unternehmen wollen unsere Daten kontrollieren.
Wir wollen selbst die volle Kontrolle über unsere **data** haben.
### Unsere Mission
Digitale Unabhängigkeit und Privatsphäre für alle verfügbar
### Ziel
Das Programm entwickeln, das es jedem ermöglicht, seine privaten Dienste für sich und seine Nachbarn einzusetzen.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Misja projektu
Niezależność i prywatność cyfrowa dostępna dla wszystkich
### Cel
Opracuj program, dzięki któremu każdy będzie mógł stworzyć prywatne usługi dla siebie i swoich bliskich.

View File

@ -1,12 +0,0 @@
### О проекте
Всё больше организаций хотят владеть нашими данными
Проект позволяет только Вам в полной мере распоряжаться собственными **данными** на своём сервере.
### Миссия проекта
Цифровая независимость и приватность, доступная каждому
### Цель
Развивать программу, которая позволит каждому создавать приватные сервисы для себя и своих близких.

View File

@ -1,12 +0,0 @@
### O nás
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Naše poslanie
Digitálna nezávislosť a súkromie dostupné pre každého
### Cieľ
Vytvorte program, ktorý umožní každému vytvoriť súkromné služby pre seba a svojich blízkych.

View File

@ -1,12 +0,0 @@
### About us
More and more corporations want to control our data.
We want to have full control of our **data** on our own.
### Our mission
Digital independence and privacy, available to everyone
### Target
Develop the program, which will allow everyone to deploy their private services for themselves and their neighbours.

View File

@ -1,12 +0,0 @@
### Про нас
Все більше корпорацій хочуть контролювати свої дані.
Ми хочемо мати повний контроль над нашими.
### Наша місія
Цифрова незалежність і конфіденційність доступні кожному
### Ціль
Розробити програму, яка дозволить кожному розгорнути свої приватні послуги для себе та їх сусідів.

View File

@ -36,29 +36,41 @@
"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",
"about_project": "About us",
"about_application": "About",
"onboarding": "Onboarding",
"create_ssh_key": "Superuser SSH keys",
"console": "Console",
"application_settings": "Application settings"
"create_ssh_key": "Superuser SSH keys"
},
"console_page": {
"title": "Console",
"waiting": "Waiting for initialization…",
"copy": "Copy"
},
"about_us_page": {
"title": "About us"
},
"about_application_page": {
"title": "About",
"application_version_text": "Application version {}",
"api_version_text": "Server API version {}",
"title": "About & support",
"versions": "Versions",
"application_version_text": "Application version",
"api_version_text": "Server API version",
"open_source_licenses": "Open source licenses",
"links": "Links",
"website": "Our website",
"documentation": "Documentation",
"matrix_channel": "Matrix channel",
"telegram_channel": "Telegram channel",
"get_support": "Get support",
"matrix_support_chat": "Matrix support chat",
"telegram_support_chat": "Telegram support chat",
"email_support": "Email support",
"contribute": "Contribute",
"source_code": "Source code",
"bug_report": "Report a bug",
"bug_report_subtitle": "Due to spam, manual account confirmation is required. Contact us in the support chat to activate your account.",
"help_translate": "Help us translate",
"matrix_contributors_chat": "Matrix contributors chat",
"telegram_contributors_chat": "Telegram contributors chat",
"privacy_policy": "Privacy policy"
},
"application_settings": {
@ -305,6 +317,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 +406,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 +607,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 +621,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 +657,4 @@
"reset_onboarding_description": "Reset onboarding switch to show onboarding screen again",
"cubit_statuses": "Cubit loading statuses"
}
}
}

View File

@ -0,0 +1,15 @@
### Features
- Enabled the following languages:
- Azerbaijani
- Belarusian
- Hebrew
- Latvian
- Macedonian
- Slovak
- Slovenian
### Bug Fixes
- **Hetzner**: Fixed an issue where could not resize a volume on Hetzner ([#456](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/456), resolves [#455](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/455))
- **DNS**: Make sure that we notice domain ownership lost ([#441](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/441), resolves [#390](https://git.selfprivacy.org/SelfPrivacy/selfprivacy.org.app/issues/390))

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

@ -1 +0,0 @@

View File

@ -17,10 +17,6 @@ class StrayDeerPainter extends CustomPainter {
final Color deerSkin =
const Color(0xffe0ac9c).harmonizeWith(colorScheme.primary);
print('deerSkin: $deerSkin');
print('colorScheme.primary: ${colorScheme.primary}');
print('colorPalette.tertiary.get(10): ${colorPalette.tertiary.get(50)}');
final Path path0 = Path();
path0.moveTo(size.width * 0.6099773, size.height * 0.6719577);
path0.lineTo(size.width * 0.6088435, size.height * 0.6719577);

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -11,6 +11,7 @@ mixin VolumeApi on GraphQLApiMap {
if (response.hasException) {
print(response.exception.toString());
}
// TODO: Rewrite to use fromGraphQL
volumes = response.data!['storage']['volumes']
.map<ServerDiskVolume>((final e) => ServerDiskVolume.fromJson(e))
.toList();
@ -59,17 +60,18 @@ mixin VolumeApi on GraphQLApiMap {
Future<GenericResult<String?>> migrateToBinds(
final Map<String, String> serviceToDisk,
final String fallbackDrive,
) async {
GenericResult<String?>? mutation;
try {
final GraphQLClient client = await getClient();
final input = Input$MigrateToBindsInput(
bitwardenBlockDevice: serviceToDisk['bitwarden']!,
emailBlockDevice: serviceToDisk['mailserver']!,
giteaBlockDevice: serviceToDisk['gitea']!,
nextcloudBlockDevice: serviceToDisk['nextcloud']!,
pleromaBlockDevice: serviceToDisk['pleroma']!,
bitwardenBlockDevice: serviceToDisk['bitwarden'] ?? fallbackDrive,
emailBlockDevice: serviceToDisk['email'] ?? fallbackDrive,
giteaBlockDevice: serviceToDisk['gitea'] ?? fallbackDrive,
nextcloudBlockDevice: serviceToDisk['nextcloud'] ?? fallbackDrive,
pleromaBlockDevice: serviceToDisk['pleroma'] ?? fallbackDrive,
);
final variables = Variables$Mutation$MigrateToBinds(input: input);
final migrateMutation =

View File

@ -547,7 +547,7 @@ class HetznerApi extends RestApiMap {
resizeVolumeResponse = await client.post(
'/volumes/${volume.id}/actions/resize',
data: {
'size': size.gibibyte,
'size': size.gibibyte.floor(),
},
);
success =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,101 @@
import 'dart:async';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.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();
}
Future<void> migrateToBinds(final Map<String, String> serviceToDisk) async {
final fallbackDrive = getIt<ApiConnectionRepository>()
.apiData
.volumes
.data
?.where((final drive) => drive.root)
.first
.name ??
'sda1';
final result = await getIt<ApiConnectionRepository>()
.api
.migrateToBinds(serviceToDisk, fallbackDrive);
if (result.data == null) {
getIt<NavigationService>()
.showSnackBar(result.message!, behavior: SnackBarBehavior.floating);
return;
}
}
@override
void onChange(final Change<ServerJobsState> change) {
super.onChange(change);
}
@override
Future<void> close() {
_apiDataSubscription?.cancel();
return super.close();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,279 +0,0 @@
import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/api_maps/rest_maps/backblaze.dart';
import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart';
import 'package:selfprivacy/logic/models/backup.dart';
import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart';
import 'package:selfprivacy/logic/models/hive/backups_credential.dart';
import 'package:selfprivacy/logic/models/initialize_repository_input.dart';
import 'package:selfprivacy/logic/models/service.dart';
part 'backups_state.dart';
class BackupsCubit extends ServerInstallationDependendCubit<BackupsState> {
BackupsCubit(final ServerInstallationCubit serverInstallationCubit)
: super(
serverInstallationCubit,
const BackupsState(preventActions: true),
);
final ServerApi api = ServerApi();
final BackblazeApi backblaze = BackblazeApi();
@override
Future<void> load() async {
if (serverInstallationCubit.state is ServerInstallationFinished) {
final BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
final BackupConfiguration? backupConfig =
await api.getBackupsConfiguration();
final List<Backup> backups = await api.getBackups();
backups.sort((final a, final b) => b.time.compareTo(a.time));
emit(
state.copyWith(
backblazeBucket: bucket,
isInitialized: backupConfig?.isInitialized,
autobackupPeriod: backupConfig?.autobackupPeriod ?? Duration.zero,
autobackupQuotas: backupConfig?.autobackupQuotas,
backups: backups,
preventActions: false,
refreshing: false,
),
);
}
}
Future<void> initializeBackups() async {
emit(state.copyWith(preventActions: true));
final String? encryptionKey =
(await api.getBackupsConfiguration())?.encryptionKey;
if (encryptionKey == null) {
getIt<NavigationService>()
.showSnackBar("Couldn't get encryption key from your server.");
emit(state.copyWith(preventActions: false));
return;
}
final BackblazeBucket bucket;
if (state.backblazeBucket == null) {
final String domain = serverInstallationCubit
.state.serverDomain!.domainName
.replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-');
final int serverId = serverInstallationCubit.state.serverDetails!.id;
String bucketName =
'${DateTime.now().millisecondsSinceEpoch}-$serverId-$domain';
if (bucketName.length > 49) {
bucketName = bucketName.substring(0, 49);
}
final String bucketId = await backblaze.createBucket(bucketName);
final BackblazeApplicationKey key = await backblaze.createKey(bucketId);
bucket = BackblazeBucket(
bucketId: bucketId,
bucketName: bucketName,
applicationKey: key.applicationKey,
applicationKeyId: key.applicationKeyId,
encryptionKey: encryptionKey,
);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
emit(state.copyWith(backblazeBucket: bucket));
} else {
bucket = state.backblazeBucket!;
}
final GenericResult result = await api.initializeRepository(
InitializeRepositoryInput(
provider: BackupsProviderType.backblaze,
locationId: bucket.bucketId,
locationName: bucket.bucketName,
login: bucket.applicationKeyId,
password: bucket.applicationKey,
),
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
return;
}
await updateBackups();
getIt<NavigationService>().showSnackBar(
'Backups repository is now initializing. It may take a while.',
);
emit(state.copyWith(preventActions: false));
}
Future<void> reuploadKey() async {
emit(state.copyWith(preventActions: true));
BackblazeBucket? bucket = getIt<ApiConfigModel>().backblazeBucket;
if (bucket == null) {
emit(state.copyWith(isInitialized: false));
} else {
String login = bucket.applicationKeyId;
String password = bucket.applicationKey;
if (login.isEmpty || password.isEmpty) {
final BackblazeApplicationKey key =
await backblaze.createKey(bucket.bucketId);
login = key.applicationKeyId;
password = key.applicationKey;
bucket = BackblazeBucket(
bucketId: bucket.bucketId,
bucketName: bucket.bucketName,
encryptionKey: bucket.encryptionKey,
applicationKey: password,
applicationKeyId: login,
);
await getIt<ApiConfigModel>().storeBackblazeBucket(bucket);
emit(state.copyWith(backblazeBucket: bucket));
}
final GenericResult result = await api.initializeRepository(
InitializeRepositoryInput(
provider: BackupsProviderType.backblaze,
locationId: bucket.bucketId,
locationName: bucket.bucketName,
login: login,
password: password,
),
);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
return;
} else {
emit(state.copyWith(preventActions: false));
getIt<NavigationService>().showSnackBar('backup.reuploaded_key'.tr());
await updateBackups();
}
}
}
@Deprecated("we don't have states")
Duration refreshTimeFromState() => const Duration(seconds: 60);
Future<void> updateBackups({final bool useTimer = false}) async {
emit(state.copyWith(refreshing: true));
final backups = await api.getBackups();
backups.sort((final a, final b) => b.time.compareTo(a.time));
final backupConfig = await api.getBackupsConfiguration();
emit(
state.copyWith(
backups: backups,
refreshTimer: refreshTimeFromState(),
refreshing: false,
isInitialized: backupConfig?.isInitialized ?? false,
autobackupPeriod: backupConfig?.autobackupPeriod,
autobackupQuotas: backupConfig?.autobackupQuotas,
),
);
if (useTimer) {
Timer(state.refreshTimer, () => updateBackups(useTimer: true));
}
}
Future<void> forceUpdateBackups() async {
emit(state.copyWith(preventActions: true));
getIt<NavigationService>().showSnackBar('backup.refetching_list'.tr());
await api.forceBackupListReload();
emit(state.copyWith(preventActions: false));
}
Future<void> createMultipleBackups(final List<Service> services) async {
emit(state.copyWith(preventActions: true));
for (final service in services) {
await api.startBackup(service.id);
}
await updateBackups();
emit(state.copyWith(preventActions: false));
}
Future<void> createBackup(final String serviceId) async {
emit(state.copyWith(preventActions: true));
await api.startBackup(serviceId);
await updateBackups();
emit(state.copyWith(preventActions: false));
}
Future<void> restoreBackup(
final String backupId,
final BackupRestoreStrategy strategy,
) async {
emit(state.copyWith(preventActions: true));
await api.restoreBackup(backupId, strategy);
emit(state.copyWith(preventActions: false));
}
Future<void> setAutobackupPeriod(final Duration? period) async {
emit(state.copyWith(preventActions: true));
final result = await api.setAutobackupPeriod(period: period?.inMinutes);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
} else {
getIt<NavigationService>()
.showSnackBar('backup.autobackup_period_set'.tr());
emit(
state.copyWith(
preventActions: false,
autobackupPeriod: period ?? Duration.zero,
),
);
}
await updateBackups();
}
Future<void> setAutobackupQuotas(final AutobackupQuotas quotas) async {
emit(state.copyWith(preventActions: true));
final result = await api.setAutobackupQuotas(quotas);
if (result.success == false) {
getIt<NavigationService>()
.showSnackBar(result.message ?? 'Unknown error');
emit(state.copyWith(preventActions: false));
} else {
getIt<NavigationService>().showSnackBar('backup.quotas_set'.tr());
emit(
state.copyWith(
preventActions: false,
autobackupQuotas: quotas,
),
);
}
await updateBackups();
}
Future<void> forgetSnapshot(final String snapshotId) async {
final result = await api.forgetSnapshot(snapshotId);
if (!result.success) {
getIt<NavigationService>().showSnackBar('jobs.generic_error'.tr());
return;
}
if (result.data == false) {
getIt<NavigationService>()
.showSnackBar('backup.forget_snapshot_error'.tr());
}
// Optimistic update
final backups = state.backups;
final index =
backups.indexWhere((final snapshot) => snapshot.id == snapshotId);
if (index != -1) {
backups.removeAt(index);
emit(state.copyWith(backups: backups));
}
await updateBackups();
}
@override
void clear() async {
emit(const BackupsState());
}
}

View File

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

View File

@ -1,36 +1,52 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:selfprivacy/config/get_it_config.dart';
import 'package:selfprivacy/logic/api_maps/graphql_maps/server_api/server_api.dart';
import 'package:selfprivacy/logic/cubit/services/services_cubit.dart';
import 'package:selfprivacy/logic/cubit/users/users_cubit.dart';
import 'package:selfprivacy/logic/models/job.dart';
import 'package:selfprivacy/logic/models/json/server_job.dart';
export 'package:provider/provider.dart';
part 'client_jobs_state.dart';
class JobsCubit extends Cubit<JobsState> {
JobsCubit({
required this.usersCubit,
required this.servicesCubit,
}) : super(JobsStateEmpty());
JobsCubit() : super(JobsStateEmpty()) {
final apiConnectionRepository = getIt<ApiConnectionRepository>();
_apiDataSubscription = apiConnectionRepository.dataStream.listen(
(final ApiData apiData) {
if (apiData.serverJobs.data != null &&
apiData.serverJobs.data!.isNotEmpty) {
_handleServerJobs(apiData.serverJobs.data!);
}
},
);
}
final ServerApi api = ServerApi();
final UsersCubit usersCubit;
final ServicesCubit servicesCubit;
StreamSubscription? _apiDataSubscription;
void addJob(final ClientJob job) {
final jobs = currentJobList;
if (job.canAddTo(jobs)) {
_updateJobsState([
...jobs,
...[job],
]);
void _handleServerJobs(final List<ServerJob> jobs) {
if (state is! JobsStateLoading) {
return;
}
if (state.rebuildJobUid == null) {
return;
}
// Find a job with the uid of the rebuild job
final ServerJob? rebuildJob = jobs.firstWhereOrNull(
(final job) => job.uid == state.rebuildJobUid,
);
if (rebuildJob == null ||
rebuildJob.status == JobStatusEnum.error ||
rebuildJob.status == JobStatusEnum.finished) {
emit((state as JobsStateLoading).finished());
}
}
void addJob(final ClientJob job) async {
emit(state.addJob(job));
}
void removeJob(final String id) {
@ -38,61 +54,153 @@ 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 if (result.success) {
emit(
JobsStateFinished(
[UpgradeServerJob(status: JobStatusEnum.finished)],
null,
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,45 @@ 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;
}
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;
}
final List<DnsRecord> allDnsRecords = await api.getDnsRecords();
final foundRecords = await validateDnsRecords(
domain,
extractDkimRecord(allDnsRecords)?.content ?? '',
allDnsRecords,
);
if (!foundRecords.success && foundRecords.message == 'link-local') {
emit(
DnsRecordsState(
dnsState: DnsRecordsStatus.error,
dnsRecords: foundRecords.data,
dnsState: foundRecords.data.any((final r) => r.isSatisfied == false)
? DnsRecordsStatus.error
: DnsRecordsStatus.good,
),
);
return;
}
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 +77,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) {
@ -158,6 +149,17 @@ class DnsRecordsCubit
message: e.toString(),
);
}
// If providerDnsRecords contains a link-local ipv6 record, return an error
if (providerDnsRecords.any(
(final r) =>
r.type == 'AAAA' && (r.content?.trim().startsWith('fe80::') ?? false),
)) {
return GenericResult(
data: foundRecords,
success: false,
message: 'link-local',
);
}
return GenericResult(
data: foundRecords,
success: true,
@ -184,14 +186,36 @@ class DnsRecordsCubit
emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing));
final List<DnsRecord> records = await api.getDnsRecords();
// If there are explicit link-local ipv6 records, remove them from the list
records.removeWhere(
(final r) =>
r.type == 'AAAA' && (r.content?.trim().startsWith('fe80::') ?? false),
);
// If there are no AAAA records, make empty copies of A records
if (!records.any((final r) => r.type == 'AAAA')) {
final recordsToAdd = records
.where((final r) => r.type == 'A')
.map(
(final r) => DnsRecord(
name: r.name,
type: 'AAAA',
content: null,
),
)
.toList();
records.addAll(recordsToAdd);
}
/// TODO: Error handling?
final ServerDomain? domain = serverInstallationCubit.state.serverDomain;
final ServerDomain? domain = getIt<ApiConnectionRepository>().serverDomain;
await ProvidersController.currentDnsProvider!.removeDomainRecords(
records: records,
domain: domain!,
);
await ProvidersController.currentDnsProvider!.createDomainRecords(
records: records,
records: records.where((final r) => r.content != null).toList(),
domain: domain,
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import 'package:graphql/client.dart';
import 'package:intl/intl.dart';
final DateFormat formatter = DateFormat('hh:mm');
/// TODO(misterfourtytwo): add equality override
class Message {
Message({this.text, this.severity = MessageSeverity.normal})
: time = DateTime.now();
@ -13,7 +12,9 @@ class Message {
final String? text;
final DateTime time;
final MessageSeverity severity;
String get timeString => formatter.format(time);
static final DateFormat _formatter = DateFormat('hh:mm');
String get timeString => _formatter.format(time);
}
enum MessageSeverity {

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(

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