Compare commits

...

62 Commits

Author SHA1 Message Date
NaiJi ✨ b8b8ac43ea Merge branch 'master' into move-title-in-cards 2024-03-04 12:42:27 +02:00
Inex Code 60c6736487 fix: Empty server confirmation screen during recovery
continuous-integration/drone/push Build is passing Details
2024-03-02 18:50:05 +02:00
Inex Code b29ee2e90e fix: Misleading value of "Do not verify TLS"
continuous-integration/drone/push Build is passing Details
2024-03-01 11:16:53 +02:00
Inex Code 6611093f48 Merge pull request 'fix: Detect the situation when we have faulty link-local IPv6 records' (#473) from inex/fix-linklocal-ipv6 into master
continuous-integration/drone/push Build was killed Details
Reviewed-on: #473
Reviewed-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
2024-03-01 11:14:24 +02:00
Inex Code 643020ebd7 fix: Detect the situation when we have faulty link-local IPv6 records 2024-03-01 11:54:27 +03:00
Inex Code c8577b3bdf fix: When using fallback upgrade, UI showed that upgrade failed
continuous-integration/drone/push Build is passing Details
2024-02-23 20:15:39 +03:00
Inex Code 212c60c613 Merge pull request 'fix: Return the binds migration interface' (#467) from inex/binds-migration into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #467
2024-02-23 18:51:52 +02:00
Inex Code a9a7b04ad5 fix: Return the binds migration interface
Turns out, there are still servers that didn't perform the binds migration. The can't perform it anymore because email changed the id. I'm getting back the option to perform the binds migration, with some fallback defaults.
2024-02-23 19:50:28 +03:00
Inex Code 490e5f92f3 refactor(ui): Code deduplication in AboutApplicationPage
continuous-integration/drone/push Build is passing Details
2024-02-23 17:56:54 +02:00
Inex Code e36cba045a feat(ui): Select device icon depending on the screen width 2024-02-23 17:56:54 +02:00
Inex Code b4f700d56a feat(ui): Select device icon depending on the platform we are runnning on 2024-02-23 17:56:54 +02:00
Inex Code 9532ddc8af feat(ui): About page now contains links 2024-02-23 17:56:54 +02:00
Inex Code 0d12b1d2d7 Merge pull request 'refactor: Introduce the API connection repository' (#440) from api-connection-refactor into master
continuous-integration/drone/push Build was killed Details
Reviewed-on: #440
Reviewed-by: NaiJi  <naiji@noreply.git.selfprivacy.org>
2024-02-23 16:49:39 +02:00
Inex Code 275e8b1f40 chore: Fixes from review 2024-02-23 17:49:10 +03:00
Inex Code 160e6d3b35 refactor: Remove unused job 2024-02-21 05:00:45 +03:00
Inex Code 7bb96b5ed0 chore: remove prints 2024-02-21 00:45:32 +03:00
Inex Code 43a339af91 refactor: Code deduplication in server data reload 2024-02-20 23:34:45 +03:00
Inex Code caa2fd3b8e refactor: Handle situation when the job has to be removed
Closes #166
2024-02-20 23:17:36 +03:00
Inex Code 4eb8f34e37 Merge remote-tracking branch 'origin/master' into api-connection-refactor 2024-02-20 20:13:19 +03:00
Inex Code 92cf2cde6d refactor: Refactor ServerDetailsCubit to use ApiConnectionRepository 2024-02-20 20:09:14 +03:00
Inex Code 9459191c09 refactor: Remove Job dependency on ClientJobsCubit 2024-02-20 20:04:39 +03:00
Inex Code 16094a3257 refactor: Rework ClientJobs cubit so it doesn't depend on other cubits
Also implemented tracking of the jobs and rebuild status
2024-02-20 19:33:24 +03:00
Inex Code fdb40fccd7 fix: Init ApiConnectionRepository after server access recovery 2024-02-14 15:59:01 +03:00
Inex Code 9a1f47711c chore: Update GraphQL schema with experimental system rebuild tracking 2024-02-12 20:20:30 +03:00
Inex Code 455b1ed7f9 refactor: Replace UsersCubit with UsersBloc 2024-02-09 18:01:05 +03:00
Inex Code e5f00f8770 refactor: Make sure that blocs use sealed classes 2024-02-09 16:54:04 +03:00
Inex Code 710b9b53dd refactor: Replace ApiDevicesCubit with DevicesBloc 2024-02-09 14:07:03 +03:00
Inex Code 27e5abfe4a Merge pull request 'feat: change NavigationDestinationLabelBehavior' (#458) from subtitles_for_menu_options into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #458
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
2024-02-08 17:18:28 +02:00
def 035fe990d0 Merge branch 'master' into subtitles_for_menu_options 2024-02-08 17:15:21 +02:00
Inex Code 3a525f0d11 refactor: Replace RecoveryKeyCubit with RecoveryKeyBloc 2024-02-08 18:08:29 +03:00
Inex Code 1daf957245 chore: Move ConnectionStatus bloc to bloc folder 2024-02-08 16:58:45 +03:00
Inex Code 0f26683758 Merge pull request 'fix: remove snackbar style notifs' (#457) from remove_snackbar_style_notif into master
continuous-integration/drone/push Build is passing Details
Reviewed-on: #457
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
2024-02-08 14:40:17 +02:00
def 087deede3a Merge branch 'master' into remove_snackbar_style_notif 2024-02-08 14:39:30 +02:00
Inex Code 46910061ed ci: Update Windows build
continuous-integration/drone/push Build was killed Details
2024-02-08 14:30:50 +02:00
aliaksei tratseuski dd81053f42 refactor(UI): Rewrite onboarding page
continuous-integration/drone/push Build was killed Details
rewrote OnboardingPage:
* decomposed into separate widgets
* now content stays centered on wide screens (set so width won't expand further than 480px)
* pageController is now properly disposed
* added some more code changes to
    * main (error widget builder)
    * brand_header (centerTitle instead of empty actions list)
    * console_page (listener callback fix, used gaps instead of SizedBox'es, added keys to list items)
    * service_page (just cleaner build method)
	* removed some dead code

Co-authored-by: Aliaksei Tratseuski <aliaksei.tratseuski@gmail.com>
Reviewed-on: #444
Co-authored-by: aliaksei tratseuski <misterfourtytwo@noreply.git.selfprivacy.org>
Co-committed-by: aliaksei tratseuski <misterfourtytwo@noreply.git.selfprivacy.org>
2024-02-08 13:59:52 +02:00
dettlaff c67661ff65 feat: change NavigationDestinationLabelBehavior 2024-02-08 00:19:27 +04:00
dettlaff ba0e247fba fix: remove SnackBarBehaviov 2024-02-08 00:06:55 +04:00
Inex Code 418d96b842 Merge pull request 'fix(hetzner): Fix the resize volume request' (#456) from hetzner-volume-resize-hotfix into master
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details
Reviewed-on: #456
2024-02-07 12:47:41 +02:00
Inex Code 74675cab23 chore: Bump version to 0.10.1 2024-02-07 13:47:22 +03:00
Inex Code 98228cfc05 fix(hetzner): Fix the resize volume request 2024-02-07 13:39:41 +03:00
Inex Code 6914b01d2a refactor: remove ProviderVolumes cubit 2024-02-06 18:21:21 +03:00
Aliaksei Tratseuski 370186030a added keys to segmented_buttons _ButtonSegment's
continuous-integration/drone/push Build is passing Details
2024-02-05 12:59:29 +02:00
Aliaksei Tratseuski 40f4f8822f chore: segmented_buttons rewrite 2024-02-05 12:59:29 +02:00
Inex Code 3b9d616045 refactor: Introduce VolumesBloc, remove ServerVolumeCubit 2024-02-01 18:30:06 +04:00
Inex Code 3222a9b500 refactor: Init blocs in initState and not in widget build 2024-01-31 18:06:49 +04:00
Inex Code e330f71b63 refactor: Optimistic state update when forgetting a snapshot 2024-01-31 18:06:22 +04:00
Inex Code 1ba8f324fe refactor: Use transformers for blocs 2024-01-31 16:17:27 +04:00
Inex Code 21c0e200a9 fix: Regenerate codegen for updated model name 2024-01-31 16:03:15 +04:00
Inex Code 725c592086 refactor: Fix callbacks returning sets 2024-01-31 15:14:37 +04:00
Inex Code 02870c3149 style: Formatting 2024-01-31 15:05:12 +04:00
Inex Code fe6f900165 refactor: Move event handler registration to the beginning of blocs 2024-01-31 15:04:59 +04:00
Inex Code f46865ca71 style: Apply directives_ordering lint 2024-01-31 14:57:12 +04:00
Inex Code 31c6a18918 Merge remote-tracking branch 'origin/directives_ordering' into api-connection-refactor
# Conflicts:
#	lib/config/bloc_config.dart
#	lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart
#	lib/logic/cubit/backups/backups_cubit.dart
#	lib/logic/cubit/dns_records/dns_records_cubit.dart
#	lib/logic/cubit/providers/providers_cubit.dart
#	lib/logic/models/service.dart
#	lib/ui/pages/backups/backup_details.dart
#	lib/ui/pages/backups/change_period_modal.dart
#	lib/ui/pages/backups/change_rotation_quotas_modal.dart
#	lib/ui/pages/backups/copy_encryption_key_modal.dart
#	lib/ui/pages/more/more.dart
#	lib/ui/pages/server_storage/binds_migration/migration_process_page.dart
#	lib/ui/pages/server_storage/server_storage.dart
#	lib/ui/pages/server_storage/storage_card.dart
2024-01-31 14:50:40 +04:00
Inex Code 149969aed8 refactor: Rename ServerVolume model to reflect that it is tied to provider 2024-01-29 20:49:20 +04:00
Inex Code 9bfaf5d381 refactor: Remove usesBinds from ApiServerVolumeCubit 2024-01-29 20:45:49 +04:00
Inex Code bdd00683cd refactor: Optimistic state update when removing all finished jobs 2024-01-29 20:14:12 +04:00
Inex Code 831a0e95eb refactor: Rewrite services cubit to bloc, using ApiRepo streams 2024-01-29 19:58:37 +04:00
Inex Code a5e7725733 refactor: Rewrite backups cubit to bloc, using ApiRepo streams 2024-01-29 17:54:09 +04:00
Inex Code b1be3f24d6 refactor: Rewire cubit from depending on server_installation_cubit to the new connection manager 2024-01-26 18:46:09 +04:00
Inex Code 332e31b655 refactor: Remove binds migration 2024-01-26 14:58:59 +04:00
Inex Code 24e5c8baee refactor: Remove unused providers cubit 2024-01-26 14:49:36 +04:00
Inex Code fa21bdf034 refactor: Remove unused timer singleton 2024-01-26 14:43:44 +04:00
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