diff --git a/.metadata b/.metadata index f0274b3e..4903dbf5 100644 --- a/.metadata +++ b/.metadata @@ -1,10 +1,33 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 1aafb3a8b9b0c36241c5f5b34ee914770f015818 - channel: stable + revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + channel: beta project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + - platform: linux + create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + - platform: windows + create_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + base_revision: 5293f3cd4427b4b48ed155e7a3852c6b3c53d94a + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..9d16cb20 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,68 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - lib/generated_plugin_registrant.dart + - lib/**.g.dart + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + avoid_print: false # Uncomment to disable the `avoid_print` rule + prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + always_use_package_imports: true + invariant_booleans: true + no_adjacent_strings_in_list: true + unnecessary_statements: true + always_declare_return_types: true + always_put_required_named_parameters_first: true + always_put_control_body_on_new_line: true + avoid_escaping_inner_quotes: true + avoid_setters_without_getters: true + eol_at_end_of_file: true + prefer_constructors_over_static_methods: true + prefer_expression_function_bodies: true + prefer_final_in_for_each: true + prefer_final_locals: true + prefer_final_parameters: true + prefer_foreach: true + prefer_if_elements_to_conditional_expressions: true + prefer_mixin: true + prefer_null_aware_method_calls: true + require_trailing_commas: true + sized_box_shrink_expand: true + sort_constructors_first: true + unnecessary_await_in_return: true + unnecessary_null_checks: true + unnecessary_parenthesis: true + use_enums: true + use_if_null_to_convert_nulls_to_bools: true + use_is_even_rather_than_modulo: true + use_late_for_private_fields_and_variables: true + use_named_constants: true + use_setters_to_change_properties: true + use_string_buffers: true + use_super_parameters: true + use_to_and_as_if_applicable: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/app/build.gradle b/android/app/build.gradle index 1b726e4e..0963724e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,12 +26,22 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + lintOptions { disable 'InvalidPackage' } @@ -39,7 +49,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "pro.kherel.selfprivacy" - minSdkVersion 18 + minSdkVersion 21 targetSdkVersion 31 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..3db14bb5 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/build.gradle b/android/build.gradle index b3afb285..31e95773 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.5.10' + ext.kotlin_version = '1.6.10' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f8865307..cc5527d7 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/assets/markdown/how_fallback_old-en.md b/assets/markdown/how_fallback_old-en.md new file mode 100644 index 00000000..c12504e7 --- /dev/null +++ b/assets/markdown/how_fallback_old-en.md @@ -0,0 +1,3 @@ +In the next window, enter the token obtained from the console of the previous version of the application. + +Enter it without the word *Bearer*. diff --git a/assets/markdown/how_fallback_old-ru.md b/assets/markdown/how_fallback_old-ru.md new file mode 100644 index 00000000..1d0a43f7 --- /dev/null +++ b/assets/markdown/how_fallback_old-ru.md @@ -0,0 +1,3 @@ +Введите в следующем окне токен, полученный из консоли прошлой версии приложения. + +Вводить нужно без слова *Bearer*. diff --git a/assets/markdown/how_fallback_ssh-en.md b/assets/markdown/how_fallback_ssh-en.md new file mode 100644 index 00000000..ce90e76a --- /dev/null +++ b/assets/markdown/how_fallback_ssh-en.md @@ -0,0 +1,19 @@ +Login as root user to your server and look at the contents of the file `/etc/nixos/userdata/tokens.json` + +```sh +cat /etc/nixos/userdata/tokens.json +``` + +This file will have a similar construction: + +```json +{ + "tokens": [ + { + "token": "token_to_copy", + "name": "device_name", + "date": "date" + } +``` + +Copy the token from the file and paste it in the next window. diff --git a/assets/markdown/how_fallback_ssh-ru.md b/assets/markdown/how_fallback_ssh-ru.md new file mode 100644 index 00000000..a1737f08 --- /dev/null +++ b/assets/markdown/how_fallback_ssh-ru.md @@ -0,0 +1,19 @@ +Войдите как root пользователь на свой сервер и посмотрите содержимое файла `/etc/nixos/userdata/tokens.json` + +```sh +cat /etc/nixos/userdata/tokens.json +``` + +В этом файле будет схожая конструкция: + +```json +{ + "tokens": [ + { + "token": "токен_который_надо_скопировать", + "name": "имя_устройства", + "date": "дата" + } +``` + +Скопируйте токен из файла и вставьте в следующем окне. diff --git a/assets/markdown/how_fallback_terminal-en.md b/assets/markdown/how_fallback_terminal-en.md new file mode 100644 index 00000000..77c97efa --- /dev/null +++ b/assets/markdown/how_fallback_terminal-en.md @@ -0,0 +1,26 @@ +In the Hetzner server control panel, go to the **Rescue** tab. Then, click on **Enable rescue & power cycle**. + +In *Choose a Recue OS* select **linux64**, and in *SSH Key* select your key if it has been added to your Hetzner account. + +Click **Enable rescue & power cycle** and wait for the server to reboot. The login and password will be displayed on the screen. Login to the root user using your login and password information. + +Mount your server file system and see the contents of the token file: + +```sh +mount /dev/sda1 /mnt +cat /mnt/etc/nixos/userdata/tokens.json +``` + +This file will have a similar construction: + +```json +{ + "tokens": [ + { + "token": "token_to_copy", + "name": "device_name", + "date": "date" + } +``` + +Copy the token from the file and paste it in the next window. diff --git a/assets/markdown/how_fallback_terminal-ru.md b/assets/markdown/how_fallback_terminal-ru.md new file mode 100644 index 00000000..6681191e --- /dev/null +++ b/assets/markdown/how_fallback_terminal-ru.md @@ -0,0 +1,26 @@ +В панели управления сервером в Hetzner перейдите во вкладку **Rescue**. Затем, нажмите на кнопку **Enable rescue & power cycle**. + +В поле *Choose a Recue OS* выберите **linux64**, а в *SSH Key* свой ключ, если он был добавлен в ваш аккаунт Hetzner. + +Нажмите **Enable rescue & power cycle** и подождите перезагрузки сервера. На экране будет отображён пароль для входа. Войдите в root пользователя используя данные логин и пароль. + +Примонтируйте файловую систему вашего сервера и посмотрите содержимое файла с токенами: + +```sh +mount /dev/sda1 /mnt +cat /mnt/etc/nixos/userdata/tokens.json +``` + +В этом файле будет схожая конструкция: + +```json +{ + "tokens": [ + { + "token": "токен_который_надо_скопировать", + "name": "имя_устройства", + "date": "дата" + } +``` + +Скопируйте токен из файла и вставьте в следующем окне. diff --git a/assets/translations/en.json b/assets/translations/en.json index 73c3575e..46cee9b8 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -21,13 +21,15 @@ "saving": "Saving..", "nickname": "Nickname", "loading": "Loading...", - "later": "I will setup it later", + "later": "Skip to setup later", + "connect_to_existing": "Connect to an existing server", "reset": "Reset", "details": "Details", "no_data": "No data", "wait": "Wait", "remove": "Remove", - "apply": "Apply" + "apply": "Apply", + "done": "Done" }, "more": { "_comment": "'More' tab", @@ -35,7 +37,7 @@ "about_project": "About us", "about_app": "About application", "onboarding": "Onboarding", - "create_ssh_key": "Create ssh key", + "create_ssh_key": "Create SSH key", "generate_key": "Generate key", "generate_key_text": "You can generate ssh key", "console": "Console", @@ -255,7 +257,7 @@ "initializing": { "_comment": "initializing page", "1": "Connect a server", - "2": "Here, your data and SelfPrivacy services wiil reside", + "2": "A place where your data and SelfPrivacy services will reside:", "how": "How to obtain API token", "3": "Connect CloudFlare", "4": "To manage your domain's DNS", @@ -273,7 +275,6 @@ "15": "Server created. DNS checks and server boot in progress...", "16": "Until the next check: ", "17": "Check", - "18": "How to obtain Hetzner API Token", "19": "1 Go via this link ", "20": "\n", "21": "One more restart to apply your security certificates.", @@ -282,6 +283,94 @@ "finish": "Everything is initialized", "checks": "Checks have been completed \n{} ouf of {}" }, + "recovering": { + "recovery_main_header": "Connect to an existing server", + "domain_recovery_description": "Enter a server domain you want to get access for:", + "domain_recover_placeholder": "Your domain", + "domain_recover_error": "Server with such domain was not found", + "method_select_description": "Select a recovery method:", + "method_select_other_device": "I have access on another device", + "method_select_recovery_key": "I have a recovery key", + "method_select_nothing": "I don't have any of that", + "method_device_description": "Open the application on another device, then go to the devices page. Press \"Add device\" to receive your token.", + "method_device_button": "I have received my token", + "method_device_input_description": "Enter your authorization token", + "method_device_input_placeholder": "Token", + "method_recovery_input_description": "Enter your recovery key", + "fallback_select_description": "What exactly do you have? Pick the first available option:", + "fallback_select_token_copy": "Copy of auth token from other version of the application.", + "fallback_select_root_ssh": "Root SSH access to the server.", + "fallback_select_provider_console": "Access to the server console of my prodiver.", + "authorization_failed": "Couldn't log in with this key", + "fallback_select_provider_console_hint": "For example: Hetzner.", + "hetzner_connected": "Connect to Hetzner", + "hetzner_connected_description": "Communication established. Enter Hetzner token with access to {}:", + "hetzner_connected_placeholder": "Hetzner token", + "confirm_server": "Confirm server", + "confirm_server_description": "Found your server! Confirm it is correct.", + "confirm_server_accept": "Yes! That's it", + "confirm_server_decline": "Choose a different server", + "choose_server": "Choose your server", + "choose_server_description": "We couldn't figure out which server your are trying to connect to.", + "no_servers": "There is no available servers on your account.", + "domain_not_available_on_token": "Selected domain is not available on this token.", + "modal_confirmation_title": "Is it really your server?", + "modal_confirmation_description": "If you connect to a wrong server you may lose all your data.", + "modal_confirmation_dns_valid": "Reverse DNS is valid", + "modal_confirmation_dns_invalid": "Reverse DNS points to another domain", + "modal_confirmation_ip_valid": "IP is the same as in DNS record", + "modal_confirmation_ip_invalid": "IP is not the same as in DNS record", + "confirm_cloudflare": "Connect to CloudFlare", + "confirm_cloudflare_description": "Enter a Cloudflare token with access to {}:", + "confirm_backblaze": "Connect to Backblaze", + "confirm_backblaze_description": "Enter a Backblaze token with access to backup storage:" + }, + "devices": { + "main_screen": { + "header": "Devices", + "description": "These devices have full access to the server via SelfPrivacy app.", + "this_device": "This device", + "other_devices": "Other devices", + "authorize_new_device": "Authorize new device", + "access_granted_on" : "Access granted on {}", + "tip": "Press on the device to revoke access." + }, + "add_new_device_screen": { + "header": "Authorizing new device", + "description": "Enter the key on the device you want to authorize:", + "please_wait": "Please wait", + "tip": "The key is valid for 10 minutes.", + "expired": "The key has expired.", + "get_new_key": "Get new key" + }, + "revoke_device_alert": { + "header": "Revoke access?", + "description": "The device {} will no longer have access to the server.", + "yes": "Revoke", + "no": "Cancel" + } + }, + "recovery_key": { + "key_connection_error": "Couldn't connect to the server.", + "key_synchronizing": "Synchronizing...", + "key_main_header": "Recovery key", + "key_main_description": "Is needed for SelfPrivacy authorization when all your other authorized devices aren't available.", + "key_amount_toggle": "Limit by number of uses", + "key_amount_field_title": "Max number of uses", + "key_duedate_toggle": "Limit by time", + "key_duedate_field_title": "Due date of expiration", + "key_receive_button": "Receive key", + "key_valid": "Your key is valid", + "key_invalid": "Your key is no longer valid", + "key_valid_until": "Valid until {}", + "key_valid_for": "Valid for {} uses", + "key_creation_date": "Created on {}", + "key_replace_button": "Generate new key", + "key_receiving_description": "Write down this key and put to a safe place. It is used to restore full access to your server:", + "key_receiving_info": "The key will never ever be shown again, but you will be able to replace it with another one.", + "key_receiving_done": "Done!", + "generation_error": "Couldn't generate a recovery key. {}" + }, "modals": { "_comment": "messages in modals", "1": "Server with such name, already exist", @@ -293,7 +382,9 @@ "7": "Yes", "8": "Remove task", "9": "Reboot", - "yes": "Yes" + "10": "You cannot use this API for domains with such TLD.", + "yes": "Yes", + "no": "No" }, "timer": { "sec": "{} sec" diff --git a/assets/translations/ru.json b/assets/translations/ru.json index ee085ed4..68665e26 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -21,13 +21,15 @@ "saving": "Сохранение…", "nickname": "Никнейм", "loading": "Загрузка", - "later": "Настрою потом", + "later": "Пропустить и настроить потом", + "connect_to_existing": "Подключиться к существующему серверу", "reset": "Сбросить", "details": "Детальная информация", "no_data": "Нет данных", "wait": "Загрузка", "remove": "Удалить", - "apply": "Подать" + "apply": "Подать", + "done": "Готово" }, "more": { "_comment": "вкладка ещё", @@ -283,6 +285,90 @@ "finish": "Всё инициализировано.", "checks": "Проверок выполнено: \n{} / {}" }, + "recovering": { + "recovery_main_header": "Подключиться к существующему серверу", + "domain_recovery_description": "Введите домен, по которому вы хотите получить доступ к серверу:", + "domain_recover_placeholder": "Домен", + "domain_recover_error": "Не удалось найти сервер с таким доменом", + "method_select_description": "Выберите способ входа:", + "method_select_other_device": "У меня есть доступ на другом устройстве", + "method_select_recovery_key": "У меня есть ключ восстановления", + "method_select_nothing": "У меня ничего из этого нет", + "method_device_description": "Откройте приложение на другом устройстве и откройте экран управления устройствами. Нажмите \"Добавить устройство\" чтобы получить токен для авторизации.", + "method_device_button": "Я получил токен", + "method_device_input_description": "Введите ваш токен авторизации", + "method_device_input_placeholder": "Токен", + "method_recovery_input_description": "Введите ваш токен восстановления", + "fallback_select_description": "Что у вас из этого есть? Выберите первое, что подходит:", + "fallback_select_token_copy": "Копия токена авторизации из другой версии приложения.", + "fallback_select_root_ssh": "Root доступ к серверу по SSH.", + "fallback_select_provider_console": "Доступ к консоли хостинга.", + "authorization_failed": "Не удалось войти с этим ключом", + "fallback_select_provider_console_hint": "Например, Hetzner.", + "hetzner_connected": "Подключение к Hetzner", + "hetzner_connected_description": "Связь с сервером установлена. Введите токен Hetzner с доступом к {}:", + "hetzner_connected_placeholder": "Hetzner токен", + "confirm_server": "Подтвердите сервер", + "confirm_server_description": "Нашли сервер! Подтвердите, что это он:", + "confirm_server_accept": "Да, это он", + "confirm_server_decline": "Выбрать другой сервер", + "choose_server": "Выберите сервер", + "choose_server_description": "Не удалось определить, с каким сервером вы устанавливаете связь.", + "no_servers": "На вашем аккаунте нет доступных серверов.", + "domain_not_available_on_token": "Введённый токен не имеет доступа к нужному домену.", + "modal_confirmation_title": "Это действительно ваш сервер?", + "modal_confirmation_description": "Подключение к неправильному серверу может привести к деструктивным последствиям.", + "confirm_cloudflare": "Подключение к Cloudflare", + "confirm_cloudflare_description": "Введите токен Cloudflare, который имеет права на {}:", + "confirm_backblze": "Подключение к Backblaze", + "confirm_backblaze_description": "Введите токен Backblaze, который имеет права на хранилище резервных копий:" + }, + "devices": { + "main_screen": { + "header": "Устройства", + "description": "Эти устройства имеют полный доступ к управлению сервером через приложение SelfPrivacy.", + "this_device": "Это устройство", + "other_devices": "Другие устройства", + "authorize_new_device": "Авторизовать новое устройство", + "access_granted_on" : "Доступ выдан {}", + "tip": "Нажмите на устройство, чтобы отозвать доступ." + }, + "add_new_device_screen": { + "header": "Авторизация нового устройства", + "description": "Введите этот ключ на новом устройстве:", + "please_wait": "Пожалуйста, подождите", + "tip": "Ключ действителен 10 минут.", + "expired": "Срок действия ключа истёк.", + "get_new_key": "Получить новый ключ" + }, + "revoke_device_alert": { + "header": "Отозвать доступ?", + "description": "Устройство {} больше не сможет управлять сервером.", + "yes": "Отозвать", + "no": "Отмена" + } + }, + "recovery_key": { + "key_connection_error": "Не удалось соединиться с сервером", + "key_synchronizing": "Синхронизация...", + "key_main_header": "Ключ восстановления", + "key_main_description": "Требуется для авторизации SelfPrivacy, когда авторизованные устройства недоступны.", + "key_amount_toggle": "Ограничить использования", + "key_amount_field_title": "Макс. кол-во использований", + "key_duedate_toggle": "Ограничить срок использования", + "key_duedate_field_title": "Дата окончания срока", + "key_receive_button": "Получить ключ", + "key_valid": "Ваш ключ действителен", + "key_invalid": "Ваш ключ больше не действителен", + "key_valid_until": "Действителен до {}", + "key_valid_for": "Можно использовать ещё {} раз", + "key_creation_date": "Создан {}", + "key_replace_button": "Сгенерировать новый ключ", + "key_receiving_description": "Запишите этот ключ в безопасном месте. Он предоставляет полный доступ к вашему серверу:", + "key_receiving_info": "Этот ключ больше не будет показан, но вы сможете заменить его новым.", + "key_receiving_done": "Готово!", + "generation_error": "Не удалось сгенерировать ключ. {}" + }, "modals": { "_comment": "messages in modals", "1": "Сервер с таким именем уже существует", @@ -294,7 +380,9 @@ "7": "Да, удалить", "8": "Удалить задачу", "9": "Перезагрузить", - "yes": "Да" + "10": "API не поддерживает домены с таким TLD.", + "yes": "Да", + "no": "Нет" }, "timer": { "sec": "{} сек" diff --git a/fastlane/metadata/android/en-US/changelogs/0.6.0.txt b/fastlane/metadata/android/en-US/changelogs/0.6.0.txt new file mode 100644 index 00000000..b8e98f1c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0.6.0.txt @@ -0,0 +1,6 @@ +- Added support for multi-device server access from SelfPrivacy app. +- You can now create recovery token to regain the access to the server if you lose your device or the app's data. +- You can now connect to an existing server, instead of creating a new one. +- Initial support for Material Design 3 (Material You). +- App now uses your system colors on Android 12 (Material You), Windows 10 (accent color) and Linux (GTK colors). +- Minor bug fixes. diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 7b06195e..3ffd726f 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 351cf4e5..6886e9b1 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index bcdbf2e9..e2024cbd 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index 0526ab1b..3d3b2b37 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index d45144a5..3946d3b9 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; +import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.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/dns_records/dns_records_cubit.dart'; @@ -10,34 +12,56 @@ import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; class BlocAndProviderConfig extends StatelessWidget { - const BlocAndProviderConfig({Key? key, this.child}) : super(key: key); + const BlocAndProviderConfig({final super.key, this.child}); final Widget? child; @override - Widget build(BuildContext context) { - var isDark = false; - var appConfigCubit = AppConfigCubit()..load(); - var usersCubit = UsersCubit(appConfigCubit); - var servicesCubit = ServicesCubit(appConfigCubit); - var backupsCubit = BackupsCubit(appConfigCubit); - var dnsRecordsCubit = DnsRecordsCubit(appConfigCubit); + Widget build(final BuildContext context) { + const isDark = false; + final serverInstallationCubit = ServerInstallationCubit()..load(); + final usersCubit = UsersCubit(serverInstallationCubit); + final servicesCubit = ServicesCubit(serverInstallationCubit); + final backupsCubit = BackupsCubit(serverInstallationCubit); + final dnsRecordsCubit = DnsRecordsCubit(serverInstallationCubit); + final recoveryKeyCubit = RecoveryKeyCubit(serverInstallationCubit); + final apiDevicesCubit = ApiDevicesCubit(serverInstallationCubit); return MultiProvider( providers: [ BlocProvider( - create: (_) => AppSettingsCubit( + create: (final _) => AppSettingsCubit( isDarkModeOn: isDark, - isOnbordingShowing: true, + isOnboardingShowing: true, )..load(), ), - BlocProvider(create: (_) => appConfigCubit, lazy: false), - BlocProvider(create: (_) => ProvidersCubit()), - BlocProvider(create: (_) => usersCubit..load(), lazy: false), - BlocProvider(create: (_) => servicesCubit..load(), lazy: false), - BlocProvider(create: (_) => backupsCubit..load(), lazy: false), - BlocProvider(create: (_) => dnsRecordsCubit..load()), BlocProvider( - create: (_) => + create: (final _) => serverInstallationCubit, + lazy: false, + ), + BlocProvider(create: (final _) => ProvidersCubit()), + BlocProvider( + create: (final _) => usersCubit..load(), + lazy: false, + ), + BlocProvider( + create: (final _) => servicesCubit..load(), + lazy: false, + ), + BlocProvider( + create: (final _) => backupsCubit..load(), + lazy: false, + ), + BlocProvider( + create: (final _) => dnsRecordsCubit..load(), + ), + BlocProvider( + create: (final _) => recoveryKeyCubit..load(), + ), + BlocProvider( + create: (final _) => apiDevicesCubit..load(), + ), + BlocProvider( + create: (final _) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), ), ], diff --git a/lib/config/bloc_observer.dart b/lib/config/bloc_observer.dart index e54b399b..e68923c9 100644 --- a/lib/config/bloc_observer.dart +++ b/lib/config/bloc_observer.dart @@ -1,15 +1,20 @@ +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:selfprivacy/ui/components/error/error.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import './get_it_config.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; class SimpleBlocObserver extends BlocObserver { SimpleBlocObserver(); @override - void onError(BlocBase cubit, Object error, StackTrace stackTrace) { - final navigator = getIt.get().navigator!; + void onError( + final BlocBase bloc, + final Object error, + final StackTrace stackTrace, + ) { + final NavigatorState navigator = getIt.get().navigator!; navigator.push( materialRoute( @@ -19,6 +24,6 @@ class SimpleBlocObserver extends BlocObserver { ), ), ); - super.onError(cubit, error, stackTrace); + super.onError(bloc, error, stackTrace); } } diff --git a/lib/config/brand_colors.dart b/lib/config/brand_colors.dart index c68d6dd8..15d1433a 100644 --- a/lib/config/brand_colors.dart +++ b/lib/config/brand_colors.dart @@ -10,7 +10,7 @@ class BrandColors { static const Color gray3 = Color(0xFFFAFAFA); static const Color gray4 = Color(0xFFDDDDDD); static const Color gray5 = Color(0xFFEDEEF1); - static Color gray6 = Color(0xFF181818).withOpacity(0.7); + static Color gray6 = const Color(0xFF181818).withOpacity(0.7); static const Color grey7 = Color(0xFFABABAB); static const Color red1 = Color(0xFFFA0E0E); @@ -20,8 +20,8 @@ class BrandColors { static const Color green2 = Color(0xFF0F8849); - static get navBackgroundLight => white.withOpacity(0.8); - static get navBackgroundDark => black.withOpacity(0.8); + static Color get navBackgroundLight => white.withOpacity(0.8); + static Color get navBackgroundDark => black.withOpacity(0.8); static const List uninitializedGradientColors = [ Color(0xFF555555), @@ -41,14 +41,14 @@ class BrandColors { Color(0xFFEFD135), ]; - static const primary = blue; - static const headlineColor = black; - static const inactive = gray2; - static const scaffoldBackground = gray3; - static const inputInactive = gray4; + static const Color primary = blue; + static const Color headlineColor = black; + static const Color inactive = gray2; + static const Color scaffoldBackground = gray3; + static const Color inputInactive = gray4; - static const textColor1 = black; - static const textColor2 = gray1; - static const dividerColor = gray5; - static const warning = red1; + static const Color textColor1 = black; + static const Color textColor2 = gray1; + static const Color dividerColor = gray5; + static const Color warning = red1; } diff --git a/lib/config/brand_theme.dart b/lib/config/brand_theme.dart index 23755de3..3ad0623c 100644 --- a/lib/config/brand_theme.dart +++ b/lib/config/brand_theme.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'brand_colors.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; -final lightTheme = ThemeData( +final ThemeData lightTheme = ThemeData( + useMaterial3: true, primaryColor: BrandColors.primary, fontFamily: 'Inter', brightness: Brightness.light, scaffoldBackgroundColor: BrandColors.scaffoldBackground, - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( border: InputBorder.none, contentPadding: EdgeInsets.all(16), enabledBorder: OutlineInputBorder( @@ -38,7 +39,7 @@ final lightTheme = ThemeData( color: BrandColors.red1, ), ), - listTileTheme: ListTileThemeData( + listTileTheme: const ListTileThemeData( minLeadingWidth: 24.0, ), textTheme: TextTheme( @@ -47,25 +48,25 @@ final lightTheme = ThemeData( headline3: headline3Style, headline4: headline4Style, bodyText1: body1Style, - subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style + subtitle1: const TextStyle(fontSize: 15, height: 1.6), // text input style ), ); -var darkTheme = lightTheme.copyWith( +ThemeData darkTheme = lightTheme.copyWith( brightness: Brightness.dark, - scaffoldBackgroundColor: Color(0xFF202120), - iconTheme: IconThemeData(color: BrandColors.gray3), + scaffoldBackgroundColor: const Color(0xFF202120), + iconTheme: const IconThemeData(color: BrandColors.gray3), cardColor: BrandColors.gray1, - dialogBackgroundColor: Color(0xFF202120), + dialogBackgroundColor: const Color(0xFF202120), textTheme: TextTheme( headline1: headline1Style.copyWith(color: BrandColors.white), headline2: headline2Style.copyWith(color: BrandColors.white), headline3: headline3Style.copyWith(color: BrandColors.white), headline4: headline4Style.copyWith(color: BrandColors.white), bodyText1: body1Style.copyWith(color: BrandColors.white), - subtitle1: TextStyle(fontSize: 15, height: 1.6), // text input style + subtitle1: const TextStyle(fontSize: 15, height: 1.6), // text input style ), - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( labelStyle: TextStyle(color: BrandColors.white), hintStyle: TextStyle(color: BrandColors.white), border: OutlineInputBorder( @@ -81,6 +82,7 @@ var darkTheme = lightTheme.copyWith( ), ); -final paddingH15V30 = EdgeInsets.symmetric(horizontal: 15, vertical: 30); +const EdgeInsets paddingH15V30 = + EdgeInsets.symmetric(horizontal: 15, vertical: 30); -final paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); +const EdgeInsets paddingH15V0 = EdgeInsets.symmetric(horizontal: 15); diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart index eb5c0902..6961ea94 100644 --- a/lib/config/get_it_config.dart +++ b/lib/config/get_it_config.dart @@ -2,7 +2,6 @@ import 'package:get_it/get_it.dart'; import 'package:selfprivacy/logic/get_it/api_config.dart'; import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/get_it/navigation.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; import 'package:selfprivacy/logic/get_it/timer.dart'; export 'package:selfprivacy/logic/get_it/api_config.dart'; @@ -10,14 +9,13 @@ 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.instance; +final GetIt getIt = GetIt.instance; Future getItSetup() async { getIt.registerSingleton(NavigationService()); getIt.registerSingleton(ConsoleModel()); getIt.registerSingleton(TimerModel()); - getIt.registerSingleton(SSHModel()..init()); getIt.registerSingleton(ApiConfigModel()..init()); await getIt.allReady(); diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index e7ed84e3..03355311 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -3,72 +3,119 @@ import 'dart:typed_data'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.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'; class HiveConfig { static Future init() async { await Hive.initFlutter(); Hive.registerAdapter(UserAdapter()); - Hive.registerAdapter(HetznerServerDetailsAdapter()); - Hive.registerAdapter(CloudFlareDomainAdapter()); + Hive.registerAdapter(ServerHostingDetailsAdapter()); + Hive.registerAdapter(ServerDomainAdapter()); Hive.registerAdapter(BackblazeCredentialAdapter()); Hive.registerAdapter(BackblazeBucketAdapter()); - Hive.registerAdapter(HetznerDataBaseAdapter()); + Hive.registerAdapter(ServerVolumeAdapter()); - await Hive.openBox(BNames.appSettings); - await Hive.openBox(BNames.users); - await Hive.openBox(BNames.servicesState); + Hive.registerAdapter(DnsProviderAdapter()); + Hive.registerAdapter(ServerProviderAdapter()); - var cipher = HiveAesCipher(await getEncryptedKey(BNames.key)); - await Hive.openBox(BNames.appConfig, encryptionCipher: cipher); + await Hive.openBox(BNames.appSettingsBox); - var sshCipher = HiveAesCipher(await getEncryptedKey(BNames.sshEnckey)); - await Hive.openBox(BNames.sshConfig, encryptionCipher: sshCipher); + final HiveAesCipher cipher = HiveAesCipher( + await getEncryptedKey(BNames.serverInstallationEncryptionKey), + ); + + await Hive.openBox(BNames.usersDeprecated); + await Hive.openBox(BNames.usersBox, encryptionCipher: cipher); + + final Box deprecatedUsers = Hive.box(BNames.usersDeprecated); + if (deprecatedUsers.isNotEmpty) { + final Box users = Hive.box(BNames.usersBox); + users.addAll(deprecatedUsers.values.toList()); + deprecatedUsers.clear(); + } + + await Hive.openBox(BNames.serverInstallationBox, encryptionCipher: cipher); } - static Future getEncryptedKey(String encKey) async { - final secureStorage = FlutterSecureStorage(); - var hasEncryptionKey = await secureStorage.containsKey(key: encKey); + static Future getEncryptedKey(final String encKey) async { + const FlutterSecureStorage secureStorage = FlutterSecureStorage(); + final bool hasEncryptionKey = await secureStorage.containsKey(key: encKey); if (!hasEncryptionKey) { - var key = Hive.generateSecureKey(); + final List key = Hive.generateSecureKey(); await secureStorage.write(key: encKey, value: base64UrlEncode(key)); } - String? string = await secureStorage.read(key: encKey); + final String? string = await secureStorage.read(key: encKey); return base64Url.decode(string!); } } +/// Mappings for the different boxes and their keys class BNames { - static String appConfig = 'appConfig'; + /// App settings box. Contains app settings like [isDarkModeOn], [isOnboardingShowing] + static String appSettingsBox = 'appSettings'; + + /// A boolean field of [appSettingsBox] box. static String isDarkModeOn = 'isDarkModeOn'; - static String isOnbordingShowing = 'isOnbordingShowing'; - static String users = 'users'; + + /// A boolean field of [appSettingsBox] box. + static String isOnboardingShowing = 'isOnboardingShowing'; + + /// Encryption key to decrypt [serverInstallationBox] and [usersBox] box. + static String serverInstallationEncryptionKey = 'key'; + + /// Server installation box. Contains server details and provider tokens. + static String serverInstallationBox = 'appConfig'; + + /// A List field of [serverInstallationBox] box. static String rootKeys = 'rootKeys'; - static String appSettings = 'appSettings'; - static String servicesState = 'servicesState'; - - static String key = 'key'; - static String sshEnckey = 'sshEngkey'; - - static String cloudFlareDomain = 'cloudFlareDomain'; - static String hetznerKey = 'hetznerKey'; - static String cloudFlareKey = 'cloudFlareKey'; - static String rootUser = 'rootUser'; - static String hetznerServer = 'hetznerServer'; + /// A boolean field of [serverInstallationBox] box. static String hasFinalChecked = 'hasFinalChecked'; + + /// A boolean field of [serverInstallationBox] box. static String isServerStarted = 'isServerStarted'; - static String backblazeKey = 'backblazeKey'; + + /// A [ServerDomain] field of [serverInstallationBox] box. + static String serverDomain = 'cloudFlareDomain'; + + /// A String field of [serverInstallationBox] box. + static String hetznerKey = 'hetznerKey'; + + /// A String field of [serverInstallationBox] box. + static String cloudFlareKey = 'cloudFlareKey'; + + /// A [User] field of [serverInstallationBox] box. + static String rootUser = 'rootUser'; + + /// A [ServerHostingDetails] field of [serverInstallationBox] box. + static String serverDetails = 'hetznerServer'; + + /// A [BackblazeCredential] field of [serverInstallationBox] box. + static String backblazeCredential = 'backblazeKey'; + + /// A [BackblazeBucket] field of [serverInstallationBox] box. static String backblazeBucket = 'backblazeBucket'; + + /// A boolean field of [serverInstallationBox] box. static String isLoading = 'isLoading'; + + /// A boolean field of [serverInstallationBox] box. static String isServerResetedFirstTime = 'isServerResetedFirstTime'; + + /// A boolean field of [serverInstallationBox] box. static String isServerResetedSecondTime = 'isServerResetedSecondTime'; - static String sshConfig = 'sshConfig'; - static String sshPrivateKey = "sshPrivateKey"; - static String sshPublicKey = "sshPublicKey"; + + /// A boolean field of [serverInstallationBox] box. + static String isRecoveringServer = 'isRecoveringServer'; + + /// Deprecated users box as it is unencrypted + static String usersDeprecated = 'users'; + + /// Box with users + static String usersBox = 'usersEncrypted'; } diff --git a/lib/config/localization.dart b/lib/config/localization.dart index 09d5ac07..b8356950 100644 --- a/lib/config/localization.dart +++ b/lib/config/localization.dart @@ -3,20 +3,18 @@ import 'package:flutter/material.dart'; class Localization extends StatelessWidget { const Localization({ - Key? key, + final super.key, this.child, - }) : super(key: key); + }); final Widget? child; @override - Widget build(BuildContext context) { - return EasyLocalization( - supportedLocales: [Locale('ru'), Locale('en')], - path: 'assets/translations', - fallbackLocale: Locale('en'), - saveLocale: false, - useOnlyLangCode: true, - child: child!, - ); - } + Widget build(final BuildContext context) => EasyLocalization( + supportedLocales: const [Locale('ru'), Locale('en')], + path: 'assets/translations', + fallbackLocale: const Locale('en'), + saveLocale: false, + useOnlyLangCode: true, + child: child!, + ); } diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index f14d54e1..63b4b99c 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -1,80 +1,80 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; -import 'brand_colors.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; -final defaultTextStyle = TextStyle( +const TextStyle defaultTextStyle = TextStyle( fontSize: 15, color: BrandColors.textColor1, ); -final headline1Style = defaultTextStyle.copyWith( +final TextStyle headline1Style = defaultTextStyle.copyWith( fontSize: 40, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final headline2Style = defaultTextStyle.copyWith( +final TextStyle headline2Style = defaultTextStyle.copyWith( fontSize: 24, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final onboardingTitle = defaultTextStyle.copyWith( +final TextStyle onboardingTitle = defaultTextStyle.copyWith( fontSize: 30, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final headline3Style = defaultTextStyle.copyWith( +final TextStyle headline3Style = defaultTextStyle.copyWith( fontSize: 20, fontWeight: NamedFontWeight.extraBold, color: BrandColors.headlineColor, ); -final headline4Style = defaultTextStyle.copyWith( +final TextStyle headline4Style = defaultTextStyle.copyWith( fontSize: 18, fontWeight: NamedFontWeight.medium, color: BrandColors.headlineColor, ); -final headline4UnderlinedStyle = defaultTextStyle.copyWith( +final TextStyle headline4UnderlinedStyle = defaultTextStyle.copyWith( fontSize: 18, fontWeight: NamedFontWeight.medium, color: BrandColors.headlineColor, decoration: TextDecoration.underline, ); -final headline5Style = defaultTextStyle.copyWith( +final TextStyle headline5Style = defaultTextStyle.copyWith( fontSize: 15, fontWeight: NamedFontWeight.medium, color: BrandColors.headlineColor.withOpacity(0.8), ); -final body1Style = defaultTextStyle; -final body2Style = defaultTextStyle.copyWith( +const TextStyle body1Style = defaultTextStyle; +final TextStyle body2Style = defaultTextStyle.copyWith( color: BrandColors.textColor2, ); -final buttonTitleText = defaultTextStyle.copyWith( +final TextStyle buttonTitleText = defaultTextStyle.copyWith( color: BrandColors.white, fontSize: 16, fontWeight: FontWeight.bold, height: 1, ); -final mediumStyle = defaultTextStyle.copyWith(fontSize: 13, height: 1.53); +final TextStyle mediumStyle = + defaultTextStyle.copyWith(fontSize: 13, height: 1.53); -final smallStyle = defaultTextStyle.copyWith(fontSize: 11, height: 1.45); +final TextStyle smallStyle = + defaultTextStyle.copyWith(fontSize: 11, height: 1.45); -final linkStyle = defaultTextStyle.copyWith(color: BrandColors.blue); - -final progressTextStyleLight = TextStyle( +const TextStyle progressTextStyleLight = TextStyle( fontSize: 11, color: BrandColors.textColor1, height: 1.7, ); -final progressTextStyleDark = progressTextStyleLight.copyWith( +final TextStyle progressTextStyleDark = progressTextStyleLight.copyWith( color: BrandColors.white, ); diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart index 5e07d838..007bfd98 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -10,27 +10,32 @@ import 'package:selfprivacy/logic/models/message.dart'; abstract class ApiMap { Future getClient() async { - var dio = Dio(await options); + final Dio dio = Dio(await options); if (hasLogger) { dio.interceptors.add(PrettyDioLogger()); } dio.interceptors.add(ConsoleInterceptor()); (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = - (HttpClient client) { + (final HttpClient client) { client.badCertificateCallback = - (X509Certificate cert, String host, int port) => true; + (final X509Certificate cert, final String host, final int port) => + true; return client; }; - dio.interceptors.add(InterceptorsWrapper(onError: (DioError e, handler) { - print(e.requestOptions.path); - print(e.requestOptions.data); + dio.interceptors.add( + InterceptorsWrapper( + onError: (final DioError e, final ErrorInterceptorHandler handler) { + print(e.requestOptions.path); + print(e.requestOptions.data); - print(e.message); - print(e.response); + print(e.message); + print(e.response); - return handler.next(e); - })); + return handler.next(e); + }, + ), + ); return dio; } @@ -42,21 +47,21 @@ abstract class ApiMap { ValidateStatus? validateStatus; - void close(Dio client) { + void close(final Dio client) { client.close(); validateStatus = null; } } class ConsoleInterceptor extends InterceptorsWrapper { - void addMessage(Message message) { + void addMessage(final Message message) { getIt.get().addMessage(message); } @override - Future onRequest( - RequestOptions options, - RequestInterceptorHandler requestInterceptorHandler, + Future onRequest( + final RequestOptions options, + final RequestInterceptorHandler handler, ) async { addMessage( Message( @@ -64,13 +69,13 @@ class ConsoleInterceptor extends InterceptorsWrapper { 'request-uri: ${options.uri}\nheaders: ${options.headers}\ndata: ${options.data}', ), ); - return super.onRequest(options, requestInterceptorHandler); + return super.onRequest(options, handler); } @override - Future onResponse( - Response response, - ResponseInterceptorHandler requestInterceptorHandler, + Future onResponse( + final Response response, + final ResponseInterceptorHandler handler, ) async { addMessage( Message( @@ -80,13 +85,16 @@ class ConsoleInterceptor extends InterceptorsWrapper { ); return super.onResponse( response, - requestInterceptorHandler, + handler, ); } @override - Future onError(DioError err, ErrorInterceptorHandler handler) async { - var response = err.response; + Future onError( + final DioError err, + final ErrorInterceptorHandler handler, + ) async { + final Response? response = err.response; log(err.toString()); addMessage( Message.warn( diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index 6a56788c..8d827e78 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; class BackblazeApiAuth { BackblazeApiAuth({required this.authorizationToken, required this.apiUrl}); @@ -13,8 +13,10 @@ class BackblazeApiAuth { } class BackblazeApplicationKey { - BackblazeApplicationKey( - {required this.applicationKeyId, required this.applicationKey}); + BackblazeApplicationKey({ + required this.applicationKeyId, + required this.applicationKey, + }); final String applicationKeyId; final String applicationKey; @@ -23,11 +25,13 @@ class BackblazeApplicationKey { class BackblazeApi extends ApiMap { BackblazeApi({this.hasLogger = false, this.isWithToken = true}); + @override BaseOptions get options { - var options = BaseOptions(baseUrl: rootAddress); + final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - var backblazeCredential = getIt().backblazeCredential; - var token = backblazeCredential!.applicationKey; + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; + final String token = backblazeCredential!.applicationKey; options.headers = {'Authorization': 'Basic $token'}; } @@ -44,14 +48,17 @@ class BackblazeApi extends ApiMap { String apiPrefix = '/b2api/v2'; Future getAuthorizationToken() async { - var client = await getClient(); - var backblazeCredential = getIt().backblazeCredential; + final Dio client = await getClient(); + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; if (backblazeCredential == null) { throw Exception('Backblaze credential is null'); } final String encodedApiKey = encodedBackblazeKey( - backblazeCredential.keyId, backblazeCredential.applicationKey); - var response = await client.get( + backblazeCredential.keyId, + backblazeCredential.applicationKey, + ); + final Response response = await client.get( 'b2_authorize_account', options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), ); @@ -64,32 +71,38 @@ class BackblazeApi extends ApiMap { ); } - Future isValid(String encodedApiKey) async { - var client = await getClient(); - Response response = await client.get( - 'b2_authorize_account', - options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), - ); - close(client); - if (response.statusCode == HttpStatus.ok) { - if (response.data['allowed']['capabilities'].contains('listBuckets')) { - return true; + Future isValid(final String encodedApiKey) async { + final Dio client = await getClient(); + try { + final Response response = await client.get( + 'b2_authorize_account', + options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), + ); + if (response.statusCode == HttpStatus.ok) { + if (response.data['allowed']['capabilities'].contains('listBuckets')) { + return true; + } + return false; + } else if (response.statusCode == HttpStatus.unauthorized) { + return false; + } else { + throw Exception('code: ${response.statusCode}'); } + } on DioError { return false; - } else if (response.statusCode == HttpStatus.unauthorized) { - return false; - } else { - throw Exception('code: ${response.statusCode}'); + } finally { + close(client); } } // Create bucket - Future createBucket(String bucketName) async { - final auth = await getAuthorizationToken(); - var backblazeCredential = getIt().backblazeCredential; - var client = await getClient(); + Future createBucket(final String bucketName) async { + final BackblazeApiAuth auth = await getAuthorizationToken(); + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; + final Dio client = await getClient(); client.options.baseUrl = auth.apiUrl; - var response = await client.post( + final Response response = await client.post( '$apiPrefix/b2_create_bucket', data: { 'accountId': backblazeCredential!.keyId, @@ -97,9 +110,9 @@ class BackblazeApi extends ApiMap { 'bucketType': 'allPrivate', 'lifecycleRules': [ { - "daysFromHidingToDeleting": 30, - "daysFromUploadingToHiding": null, - "fileNamePrefix": "" + 'daysFromHidingToDeleting': 30, + 'daysFromUploadingToHiding': null, + 'fileNamePrefix': '' } ], }, @@ -116,11 +129,11 @@ class BackblazeApi extends ApiMap { } // Create a limited capability key with access to the given bucket - Future createKey(String bucketId) async { - final auth = await getAuthorizationToken(); - var client = await getClient(); + Future createKey(final String bucketId) async { + final BackblazeApiAuth auth = await getAuthorizationToken(); + final Dio client = await getClient(); client.options.baseUrl = auth.apiUrl; - var response = await client.post( + final Response response = await client.post( '$apiPrefix/b2_create_key', data: { 'accountId': getIt().backblazeCredential!.keyId, @@ -135,8 +148,9 @@ class BackblazeApi extends ApiMap { close(client); if (response.statusCode == HttpStatus.ok) { return BackblazeApplicationKey( - applicationKeyId: response.data['applicationKeyId'], - applicationKey: response.data['applicationKey']); + applicationKeyId: response.data['applicationKeyId'], + applicationKey: response.data['applicationKey'], + ); } else { throw Exception('code: ${response.statusCode}'); } diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 5aaf9dc7..9141d5fe 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -3,20 +3,40 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; -import 'package:selfprivacy/logic/models/dns_records.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; + +class DomainNotFoundException implements Exception { + DomainNotFoundException(this.message); + final String message; +} class CloudflareApi extends ApiMap { - CloudflareApi({this.hasLogger = false, this.isWithToken = true}); + CloudflareApi({ + this.hasLogger = false, + this.isWithToken = true, + this.customToken, + }); + @override + final bool hasLogger; + @override + final bool isWithToken; + final String? customToken; + + @override BaseOptions get options { - var options = BaseOptions(baseUrl: rootAddress); + final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - var token = getIt().cloudFlareKey; + final String? token = getIt().cloudFlareKey; assert(token != null); options.headers = {'Authorization': 'Bearer $token'}; } + if (customToken != null) { + options.headers = {'Authorization': 'Bearer $customToken'}; + } + if (validateStatus != null) { options.validateStatus = validateStatus!; } @@ -26,14 +46,15 @@ class CloudflareApi extends ApiMap { @override String rootAddress = 'https://api.cloudflare.com/client/v4'; - Future isValid(String token) async { - validateStatus = (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }; + Future isValid(final String token) async { + validateStatus = (final status) => + status == HttpStatus.ok || status == HttpStatus.unauthorized; - var client = await getClient(); - Response response = await client.get('/user/tokens/verify', - options: Options(headers: {'Authorization': 'Bearer $token'})); + final Dio client = await getClient(); + final Response response = await client.get( + '/user/tokens/verify', + options: Options(headers: {'Authorization': 'Bearer $token'}), + ); close(client); @@ -46,37 +67,40 @@ class CloudflareApi extends ApiMap { } } - Future getZoneId(String domain) async { - validateStatus = (status) { - return status == HttpStatus.ok || status == HttpStatus.forbidden; - }; - var client = await getClient(); - Response response = await client.get( + Future getZoneId(final String domain) async { + validateStatus = (final status) => + status == HttpStatus.ok || status == HttpStatus.forbidden; + final Dio client = await getClient(); + final Response response = await client.get( '/zones', queryParameters: {'name': domain}, ); close(client); - return response.data['result'][0]['id']; + if (response.data['result'].isEmpty) { + throw DomainNotFoundException('No domains found'); + } else { + return response.data['result'][0]['id']; + } } Future removeSimilarRecords({ - String? ip4, - required CloudFlareDomain cloudFlareDomain, + required final ServerDomain cloudFlareDomain, + final String? ip4, }) async { - var domainName = cloudFlareDomain.domainName; - var domainZoneId = cloudFlareDomain.zoneId; + final String domainName = cloudFlareDomain.domainName; + final String domainZoneId = cloudFlareDomain.zoneId; - var url = '/zones/$domainZoneId/dns_records'; + final String url = '/zones/$domainZoneId/dns_records'; - var client = await getClient(); - Response response = await client.get(url); + final Dio client = await getClient(); + final Response response = await client.get(url); - List records = response.data['result'] ?? []; - var allDeleteFutures = []; + final List records = response.data['result'] ?? []; + final List allDeleteFutures = []; - for (var record in records) { + for (final record in records) { if (record['zone_name'] == domainName) { allDeleteFutures.add( client.delete('$url/${record["id"]}'), @@ -89,28 +113,30 @@ class CloudflareApi extends ApiMap { } Future> getDnsRecords({ - required CloudFlareDomain cloudFlareDomain, + required final ServerDomain cloudFlareDomain, }) async { - var domainName = cloudFlareDomain.domainName; - var domainZoneId = cloudFlareDomain.zoneId; + final String domainName = cloudFlareDomain.domainName; + final String domainZoneId = cloudFlareDomain.zoneId; - var url = '/zones/$domainZoneId/dns_records'; + final String url = '/zones/$domainZoneId/dns_records'; - var client = await getClient(); - Response response = await client.get(url); + final Dio client = await getClient(); + final Response response = await client.get(url); - List records = response.data['result'] ?? []; - var allRecords = []; + final List records = response.data['result'] ?? []; + final List allRecords = []; - for (var record in records) { + for (final record in records) { if (record['zone_name'] == domainName) { - allRecords.add(DnsRecord( - name: record['name'], - type: record['type'], - content: record['content'], - ttl: record['ttl'], - proxied: record['proxied'], - )); + allRecords.add( + DnsRecord( + name: record['name'], + type: record['type'], + content: record['content'], + ttl: record['ttl'], + proxied: record['proxied'], + ), + ); } } @@ -119,51 +145,59 @@ class CloudflareApi extends ApiMap { } Future createMultipleDnsRecords({ - String? ip4, - required CloudFlareDomain cloudFlareDomain, + required final ServerDomain cloudFlareDomain, + final String? ip4, }) async { - var domainName = cloudFlareDomain.domainName; - var domainZoneId = cloudFlareDomain.zoneId; - var listDnsRecords = projectDnsRecords(domainName, ip4); + final String domainName = cloudFlareDomain.domainName; + final String domainZoneId = cloudFlareDomain.zoneId; + final List listDnsRecords = projectDnsRecords(domainName, ip4); + final List allCreateFutures = []; - var url = '$rootAddress/zones/$domainZoneId/dns_records'; - - var allCreateFutures = []; - var client = await getClient(); - - for (var record in listDnsRecords) { - allCreateFutures.add( - client.post( - url, - data: record.toJson(), - ), - ); + final Dio client = await getClient(); + try { + for (final DnsRecord record in listDnsRecords) { + allCreateFutures.add( + client.post( + '/zones/$domainZoneId/dns_records', + data: record.toJson(), + ), + ); + } + await Future.wait(allCreateFutures); + } on DioError catch (e) { + print(e.message); + rethrow; + } finally { + close(client); } - - await Future.wait(allCreateFutures); - close(client); } - List projectDnsRecords(String? domainName, String? ip4) { - var domainA = DnsRecord(type: 'A', name: domainName, content: ip4); + List projectDnsRecords( + final String? domainName, + final String? ip4, + ) { + final DnsRecord domainA = + DnsRecord(type: 'A', name: domainName, content: ip4); - var mx = DnsRecord(type: 'MX', name: '@', content: domainName); - var apiA = DnsRecord(type: 'A', name: 'api', content: ip4); - var cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); - var gitA = DnsRecord(type: 'A', name: 'git', content: ip4); - var meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); - var passwordA = DnsRecord(type: 'A', name: 'password', content: ip4); - var socialA = DnsRecord(type: 'A', name: 'social', content: ip4); - var vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); + final DnsRecord mx = DnsRecord(type: 'MX', name: '@', content: domainName); + final DnsRecord apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + final DnsRecord cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + final DnsRecord gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + final DnsRecord meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + final DnsRecord passwordA = + DnsRecord(type: 'A', name: 'password', content: ip4); + final DnsRecord socialA = + DnsRecord(type: 'A', name: 'social', content: ip4); + final DnsRecord vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); - var txt1 = DnsRecord( + final DnsRecord txt1 = DnsRecord( type: 'TXT', name: '_dmarc', content: 'v=DMARC1; p=none', ttl: 18000, ); - var txt2 = DnsRecord( + final DnsRecord txt2 = DnsRecord( type: 'TXT', name: domainName, content: 'v=spf1 a mx ip4:$ip4 -all', @@ -186,18 +220,20 @@ class CloudflareApi extends ApiMap { } Future setDkim( - String dkimRecordString, CloudFlareDomain cloudFlareDomain) async { - final domainZoneId = cloudFlareDomain.zoneId; - final url = '$rootAddress/zones/$domainZoneId/dns_records'; + final String dkimRecordString, + final ServerDomain cloudFlareDomain, + ) async { + final String domainZoneId = cloudFlareDomain.zoneId; + final String url = '$rootAddress/zones/$domainZoneId/dns_records'; - final dkimRecord = DnsRecord( + final DnsRecord dkimRecord = DnsRecord( type: 'TXT', name: 'selector._domainkey', content: dkimRecordString, ttl: 18000, ); - var client = await getClient(); + final Dio client = await getClient(); await client.post( url, data: dkimRecord.toJson(), @@ -207,23 +243,17 @@ class CloudflareApi extends ApiMap { } Future> domainList() async { - var url = '$rootAddress/zones?per_page=50'; - var client = await getClient(); + final String url = '$rootAddress/zones'; + final Dio client = await getClient(); - var response = await client.get( + final Response response = await client.get( url, queryParameters: {'per_page': 50}, ); close(client); return response.data['result'] - .map((el) => el['name'] as String) + .map((final el) => el['name'] as String) .toList(); } - - @override - final bool hasLogger; - - @override - final bool isWithToken; } diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 77afa5d8..4de4f36f 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -4,21 +4,23 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; -import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class HetznerApi extends ApiMap { + HetznerApi({this.hasLogger = false, this.isWithToken = true}); + @override bool hasLogger; + @override bool isWithToken; - HetznerApi({this.hasLogger = false, this.isWithToken = true}); - + @override BaseOptions get options { - var options = BaseOptions(baseUrl: rootAddress); + final BaseOptions options = BaseOptions(baseUrl: rootAddress); if (isWithToken) { - var token = getIt().hetznerKey; + final String? token = getIt().hetznerKey; assert(token != null); options.headers = {'Authorization': 'Bearer $token'}; } @@ -33,12 +35,11 @@ class HetznerApi extends ApiMap { @override String rootAddress = 'https://api.hetzner.cloud/v1'; - Future isValid(String token) async { - validateStatus = (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }; - var client = await getClient(); - Response response = await client.get( + Future isValid(final String token) async { + validateStatus = (final int? status) => + status == HttpStatus.ok || status == HttpStatus.unauthorized; + final Dio client = await getClient(); + final Response response = await client.get( '/servers', options: Options( headers: {'Authorization': 'Bearer $token'}, @@ -55,99 +56,98 @@ class HetznerApi extends ApiMap { } } - Future isFreeToCreate() async { - var client = await getClient(); - - Response serversReponse = await client.get('/servers'); - List servers = serversReponse.data['servers']; - var server = servers.firstWhere( - (el) => el['name'] == 'selfprivacy-server', - orElse: null, - ); - client.close(); - return server == null; - } - - Future createVolume() async { - var client = await getClient(); - Response dbCreateResponse = await client.post( + Future createVolume() async { + final Dio client = await getClient(); + final Response dbCreateResponse = await client.post( '/volumes', data: { - "size": 10, - "name": StringGenerators.dbStorageName(), - "labels": {"labelkey": "value"}, - "location": "fsn1", - "automount": false, - "format": "ext4" + 'size': 10, + 'name': StringGenerators.dbStorageName(), + 'labels': {'labelkey': 'value'}, + 'location': 'fsn1', + 'automount': false, + 'format': 'ext4' }, ); - var dbId = dbCreateResponse.data['volume']['id']; - return HetznerDataBase( + final dbId = dbCreateResponse.data['volume']['id']; + return ServerVolume( id: dbId, name: dbCreateResponse.data['volume']['name'], ); } - Future createServer({ - required String cloudFlareKey, - required User rootUser, - required String domainName, - required HetznerDataBase dataBase, + Future createServer({ + required final String cloudFlareKey, + required final User rootUser, + required final String domainName, + required final ServerVolume dataBase, }) async { - var client = await getClient(); + final Dio client = await getClient(); - var dbPassword = StringGenerators.dbPassword(); - var dbId = dataBase.id; + final String dbPassword = StringGenerators.dbPassword(); + final int dbId = dataBase.id; - final apiToken = StringGenerators.apiToken(); + final String apiToken = StringGenerators.apiToken(); - final hostname = getHostnameFromDomain(domainName); + final String hostname = getHostnameFromDomain(domainName); - final base64Password = + final String base64Password = base64.encode(utf8.encode(rootUser.password ?? 'PASS')); - print("hostname: $hostname"); + print('hostname: $hostname'); /// add ssh key when you need it: e.g. "ssh_keys":["kherel"] /// check the branch name, it could be "development" or "master". /// - final userdataString = + final String userdataString = "#cloud-config\nruncmd:\n- curl https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-21.05 DOMAIN='$domainName' LUSER='${rootUser.login}' ENCODED_PASSWORD='$base64Password' CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword API_TOKEN=$apiToken HOSTNAME=$hostname bash 2>&1 | tee /tmp/infect.log"; print(userdataString); - final data = { - "name": hostname, - "server_type": "cx11", - "start_after_create": false, - "image": "ubuntu-20.04", - "volumes": [dbId], - "networks": [], - "user_data": userdataString, - "labels": {}, - "automount": true, - "location": "fsn1" + final Map data = { + 'name': hostname, + 'server_type': 'cx11', + 'start_after_create': false, + 'image': 'ubuntu-20.04', + 'volumes': [dbId], + 'networks': [], + 'user_data': userdataString, + 'labels': {}, + 'automount': true, + 'location': 'fsn1' }; - print("Decoded data: $data"); + print('Decoded data: $data'); - Response serverCreateResponse = await client.post( - '/servers', - data: data, - ); + ServerHostingDetails? serverDetails; - print(serverCreateResponse.data); - client.close(); - return HetznerServerDetails( - id: serverCreateResponse.data['server']['id'], - ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], - createTime: DateTime.now(), - dataBase: dataBase, - apiToken: apiToken, - ); + try { + final Response serverCreateResponse = await client.post( + '/servers', + data: data, + ); + print(serverCreateResponse.data); + serverDetails = ServerHostingDetails( + id: serverCreateResponse.data['server']['id'], + ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], + createTime: DateTime.now(), + volume: dataBase, + apiToken: apiToken, + provider: ServerProvider.hetzner, + ); + } on DioError catch (e) { + print(e); + rethrow; + } catch (e) { + print(e); + } finally { + client.close(); + } + + return serverDetails; } - static String getHostnameFromDomain(String domain) { + static String getHostnameFromDomain(final String domain) { // Replace all non-alphanumeric characters with an underscore - var hostname = + String hostname = domain.split('.')[0].replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); if (hostname.endsWith('-')) { hostname = hostname.substring(0, hostname.length - 1); @@ -163,24 +163,24 @@ class HetznerApi extends ApiMap { } Future deleteSelfprivacyServerAndAllVolumes({ - required String domainName, + required final String domainName, }) async { - var client = await getClient(); + final Dio client = await getClient(); - final hostname = getHostnameFromDomain(domainName); + final String hostname = getHostnameFromDomain(domainName); - Response serversReponse = await client.get('/servers'); - List servers = serversReponse.data['servers']; - Map server = servers.firstWhere((el) => el['name'] == hostname); - List volumes = server['volumes']; - var laterFutures = []; + final Response serversReponse = await client.get('/servers'); + final List servers = serversReponse.data['servers']; + final Map server = servers.firstWhere((final el) => el['name'] == hostname); + final List volumes = server['volumes']; + final List laterFutures = []; - for (var volumeId in volumes) { + for (final volumeId in volumes) { await client.post('/volumes/$volumeId/actions/detach'); } - await Future.delayed(Duration(seconds: 10)); + await Future.delayed(const Duration(seconds: 10)); - for (var volumeId in volumes) { + for (final volumeId in volumes) { laterFutures.add(client.delete('/volumes/$volumeId')); } laterFutures.add(client.delete('/servers/${server['id']}')); @@ -189,20 +189,20 @@ class HetznerApi extends ApiMap { close(client); } - Future reset() async { - var server = getIt().hetznerServer!; + Future reset() async { + final ServerHostingDetails server = getIt().serverDetails!; - var client = await getClient(); + final Dio client = await getClient(); await client.post('/servers/${server.id}/actions/reset'); close(client); return server.copyWith(startTime: DateTime.now()); } - Future powerOn() async { - var server = getIt().hetznerServer!; + Future powerOn() async { + final ServerHostingDetails server = getIt().serverDetails!; - var client = await getClient(); + final Dio client = await getClient(); await client.post('/servers/${server.id}/actions/poweron'); close(client); @@ -210,16 +210,20 @@ class HetznerApi extends ApiMap { } Future> getMetrics( - DateTime start, DateTime end, String type) async { - var hetznerServer = getIt().hetznerServer; - var client = await getClient(); + final DateTime start, + final DateTime end, + final String type, + ) async { + final ServerHostingDetails? hetznerServer = + getIt().serverDetails; + final Dio client = await getClient(); - Map queryParameters = { - "start": start.toUtc().toIso8601String(), - "end": end.toUtc().toIso8601String(), - "type": type + final Map queryParameters = { + 'start': start.toUtc().toIso8601String(), + 'end': end.toUtc().toIso8601String(), + 'type': type }; - var res = await client.get( + final Response res = await client.get( '/servers/${hetznerServer!.id}/metrics', queryParameters: queryParameters, ); @@ -228,27 +232,46 @@ class HetznerApi extends ApiMap { } Future getInfo() async { - var hetznerServer = getIt().hetznerServer; - var client = await getClient(); - Response response = await client.get('/servers/${hetznerServer!.id}'); + final ServerHostingDetails? hetznerServer = + getIt().serverDetails; + final Dio client = await getClient(); + final Response response = await client.get('/servers/${hetznerServer!.id}'); close(client); return HetznerServerInfo.fromJson(response.data!['server']); } - Future createReverseDns({ - required String ip4, - required String domainName, - }) async { - var hetznerServer = getIt().hetznerServer; - var client = await getClient(); - await client.post( - '/servers/${hetznerServer!.id}/actions/change_dns_ptr', - data: { - "ip": ip4, - "dns_ptr": domainName, - }, - ); + Future> getServers() async { + final Dio client = await getClient(); + final Response response = await client.get('/servers'); close(client); + + return (response.data!['servers'] as List) + // ignore: unnecessary_lambdas + .map((final e) => HetznerServerInfo.fromJson(e)) + .toList(); + } + + Future createReverseDns({ + required final String ip4, + required final String domainName, + }) async { + final ServerHostingDetails? hetznerServer = + getIt().serverDetails; + + final Dio client = await getClient(); + try { + await client.post( + '/servers/${hetznerServer!.id}/actions/change_dns_ptr', + data: { + 'ip': ip4, + 'dns_ptr': domainName, + }, + ); + } catch (e) { + print(e); + } finally { + close(client); + } } } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 6302611e..67a0739c 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -5,81 +5,112 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.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/auto_upgrade_settings.dart'; +import 'package:selfprivacy/logic/models/json/backup.dart'; +import 'package:selfprivacy/logic/models/json/device_token.dart'; +import 'package:selfprivacy/logic/models/json/recovery_token_status.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; -import 'package:selfprivacy/logic/models/user.dart'; -import 'api_map.dart'; +import 'package:selfprivacy/logic/api_maps/api_map.dart'; class ApiResponse { + ApiResponse({ + required this.statusCode, + required this.data, + this.errorMessage, + }); final int statusCode; final String? errorMessage; final D data; bool get isSuccess => statusCode >= 200 && statusCode < 300; - - ApiResponse({ - required this.statusCode, - this.errorMessage, - required this.data, - }); } class ServerApi extends ApiMap { + ServerApi({ + this.hasLogger = false, + this.isWithToken = true, + this.overrideDomain, + this.customToken, + }); + @override bool hasLogger; + @override bool isWithToken; + String? overrideDomain; + String? customToken; - ServerApi({this.hasLogger = false, this.isWithToken = true}); - + @override BaseOptions get options { - var options = BaseOptions(); + BaseOptions options = BaseOptions(); if (isWithToken) { - var cloudFlareDomain = getIt().cloudFlareDomain; - var domainName = cloudFlareDomain!.domainName; - var apiToken = getIt().hetznerServer?.apiToken; + final ServerDomain? cloudFlareDomain = + getIt().serverDomain; + final String domainName = cloudFlareDomain!.domainName; + final String? apiToken = getIt().serverDetails?.apiToken; - options = BaseOptions(baseUrl: 'https://api.$domainName', headers: { - 'Authorization': 'Bearer $apiToken', - }); + options = BaseOptions( + baseUrl: 'https://api.$domainName', + headers: { + 'Authorization': 'Bearer $apiToken', + }, + ); + } + + if (overrideDomain != null) { + options = BaseOptions( + baseUrl: 'https://api.$overrideDomain', + headers: customToken != null + ? {'Authorization': 'Bearer $customToken'} + : null, + ); } return options; } - Future isHttpServerWorking() async { - bool res; + Future getApiVersion() async { Response response; - var client = await getClient(); + final Dio client = await getClient(); + String? apiVersion; + + try { + response = await client.get('/api/version'); + apiVersion = response.data['version']; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } + return apiVersion; + } + + Future isHttpServerWorking() async { + bool res = false; + Response response; + + final Dio client = await getClient(); try { response = await client.get('/services/status'); res = response.statusCode == HttpStatus.ok; - } catch (e) { - res = false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); } - close(client); return res; } - Future> createUser(User user) async { - var client = await getClient(); - - var makeErrorApiReponse = (int status) { - return ApiResponse( - statusCode: status, - data: User( - login: user.login, - password: user.password, - isFoundOnServer: false, - ), - ); - }; - - late Response response; + Future> createUser(final User user) async { + Response response; + final Dio client = await getClient(); try { response = await client.post( '/users', @@ -87,118 +118,179 @@ class ServerApi extends ApiMap { 'username': user.login, 'password': user.password, }, - options: Options( - contentType: 'application/json', - receiveDataWhenStatusError: true, - followRedirects: false, - validateStatus: (status) { - return (status != null) && - (status < HttpStatus.internalServerError); - }), ); - } catch (e) { - return makeErrorApiReponse(HttpStatus.internalServerError); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.error.toString(), + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: false, + ), + ); } finally { close(client); } - if ((response.statusCode != null) && - (response.statusCode == HttpStatus.created)) { - return ApiResponse( - statusCode: response.statusCode!, - data: User( - login: user.login, - password: user.password, - isFoundOnServer: true, - ), - ); + bool isFoundOnServer = false; + int code = 0; + + final bool isUserCreated = (response.statusCode != null) && + (response.statusCode == HttpStatus.created); + + if (isUserCreated) { + isFoundOnServer = true; + code = response.statusCode!; } else { - print(response.statusCode.toString() + - ": " + - (response.statusMessage ?? "")); - return makeErrorApiReponse( - response.statusCode ?? HttpStatus.internalServerError); + isFoundOnServer = false; + code = HttpStatus.notAcceptable; } + + return ApiResponse( + statusCode: code, + data: User( + login: user.login, + password: user.password, + isFoundOnServer: isFoundOnServer, + ), + ); } - Future>> getUsersList() async { - List res = []; + Future>> getUsersList({ + final withMainUser = false, + }) async { + final List res = []; Response response; - var client = await getClient(); - response = await client.get('/users'); + final Dio client = await getClient(); try { - for (var user in response.data) { + response = await client.get( + '/users', + queryParameters: withMainUser ? {'withMainUser': 'true'} : null, + ); + for (final user in response.data) { res.add(user.toString()); } + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: [], + ); } catch (e) { print(e); - res = []; + return ApiResponse( + errorMessage: e.toString(), + statusCode: HttpStatus.internalServerError, + data: [], + ); + } finally { + close(client); } - close(client); - return ApiResponse>( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, data: res, ); } - Future> addUserSshKey(User user, String sshKey) async { - Response response; + Future> addUserSshKey( + final User user, + final String sshKey, + ) async { + late Response response; - var client = await getClient(); - response = await client.post( - '/services/ssh/keys/${user.login}', - data: { - 'public_key': sshKey, - }, - ); + final Dio client = await getClient(); + try { + response = await client.post( + '/services/ssh/keys/${user.login}', + data: { + 'public_key': sshKey, + }, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; - close(client); return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: null, - errorMessage: response.data?.containsKey('error') ?? false - ? response.data['error'] - : null, ); } - Future> addRootSshKey(String ssh) async { - Response response; + Future> addRootSshKey(final String ssh) async { + late Response response; - var client = await getClient(); - response = await client.put( - '/services/ssh/key/send', - data: {"public_key": ssh}, - ); - close(client); + final Dio client = await getClient(); + try { + response = await client.put( + '/services/ssh/key/send', + data: {'public_key': ssh}, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: null, - errorMessage: response.data?.containsKey('error') ?? false - ? response.data['error'] - : null, ); } - Future>> getUserSshKeys(User user) async { + Future>> getUserSshKeys(final User user) async { List res; Response response; - var client = await getClient(); - response = await client.get('/services/ssh/keys/${user.login}'); + final Dio client = await getClient(); try { - res = (response.data as List).map((e) => e as String).toList(); + response = await client.get('/services/ssh/keys/${user.login}'); + res = (response.data as List) + .map((final e) => e as String) + .toList(); + } on DioError catch (e) { + print(e.message); + return ApiResponse>( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: [], + ); } catch (e) { - print(e); - res = []; + return ApiResponse>( + errorMessage: e.toString(), + statusCode: HttpStatus.internalServerError, + data: [], + ); + } finally { + close(client); } - close(client); + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse>( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: res, errorMessage: response.data is List ? null @@ -208,16 +300,33 @@ class ServerApi extends ApiMap { ); } - Future> deleteUserSshKey(User user, String sshKey) async { + Future> deleteUserSshKey( + final User user, + final String sshKey, + ) async { Response response; - var client = await getClient(); - response = await client.delete('/services/ssh/keys/${user.login}', - data: {"public_key": sshKey}); - close(client); + final Dio client = await getClient(); + try { + response = await client.delete( + '/services/ssh/keys/${user.login}', + data: {'public_key': sshKey}, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; return ApiResponse( - statusCode: response.statusCode ?? HttpStatus.internalServerError, + statusCode: code, data: null, errorMessage: response.data?.containsKey('error') ?? false ? response.data['error'] @@ -225,63 +334,74 @@ class ServerApi extends ApiMap { ); } - Future deleteUser(User user) async { - bool res; + Future deleteUser(final User user) async { + bool res = false; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { - response = await client.delete( - '/users/${user.login}', - options: Options( - contentType: 'application/json', - ), - ); + response = await client.delete('/users/${user.login}'); res = response.statusCode == HttpStatus.ok || response.statusCode == HttpStatus.notFound; - } catch (e) { - print(e); + } on DioError catch (e) { + print(e.message); res = false; + } finally { + close(client); } - - close(client); return res; } + @override String get rootAddress => throw UnimplementedError('not used in with implementation'); Future apply() async { - bool res; + bool res = false; Response response; - var client = await getClient(); + final Dio client = await getClient(); try { - response = await client.get( - '/system/configuration/apply', - ); - + response = await client.get('/system/configuration/apply'); res = response.statusCode == HttpStatus.ok; - } catch (e) { - print(e); + } on DioError catch (e) { + print(e.message); res = false; + } finally { + close(client); } - - close(client); return res; } - Future switchService(ServiceTypes type, bool needToTurnOn) async { - var client = await getClient(); - client.post('/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}'); - close(client); + Future switchService( + final ServiceTypes type, + final bool needToTurnOn, + ) async { + final Dio client = await getClient(); + try { + client.post( + '/services/${type.url}/${needToTurnOn ? 'enable' : 'disable'}', + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future> servicesPowerCheck() async { - var client = await getClient(); - Response response = await client.get('/services/status'); + Response response; + + final Dio client = await getClient(); + try { + response = await client.get('/services/status'); + } on DioError catch (e) { + print(e.message); + return {}; + } finally { + close(client); + } - close(client); return { ServiceTypes.passwordManager: response.data['bitwarden'] == 0, ServiceTypes.git: response.data['gitea'] == 0, @@ -291,139 +411,240 @@ class ServerApi extends ApiMap { }; } - Future uploadBackblazeConfig(BackblazeBucket bucket) async { - var client = await getClient(); - client.put( - '/services/restic/backblaze/config', - data: { - 'accountId': bucket.applicationKeyId, - 'accountKey': bucket.applicationKey, - 'bucket': bucket.bucketName, - }, - ); - close(client); + Future uploadBackblazeConfig(final BackblazeBucket bucket) async { + final Dio client = await getClient(); + try { + client.put( + '/services/restic/backblaze/config', + data: { + 'accountId': bucket.applicationKeyId, + 'accountKey': bucket.applicationKey, + 'bucket': bucket.bucketName, + }, + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future startBackup() async { - var client = await getClient(); - client.put('/services/restic/backup/create'); - close(client); + final Dio client = await getClient(); + try { + client.put('/services/restic/backup/create'); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future> getBackups() async { Response response; + List backups = []; - var client = await getClient(); + final Dio client = await getClient(); try { - response = await client.get( - '/services/restic/backup/list', - ); - return response.data.map((e) => Backup.fromJson(e)).toList(); + response = await client.get('/services/restic/backup/list'); + backups = + response.data.map((final e) => Backup.fromJson(e)).toList(); + } on DioError catch (e) { + print(e.message); } catch (e) { print(e); + } finally { + close(client); } - close(client); - return []; + return backups; } Future getBackupStatus() async { Response response; - - var client = await getClient(); - try { - response = await client.get( - '/services/restic/backup/status', - ); - return BackupStatus.fromJson(response.data); - } catch (e) { - print(e); - } - close(client); - - return BackupStatus( + BackupStatus status = BackupStatus( status: BackupStatusEnum.error, errorMessage: 'Network error', progress: 0, ); + + final Dio client = await getClient(); + try { + response = await client.get('/services/restic/backup/status'); + status = BackupStatus.fromJson(response.data); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } + return status; } Future forceBackupListReload() async { - var client = await getClient(); - client.get('/services/restic/backup/reload'); - close(client); + final Dio client = await getClient(); + try { + client.get('/services/restic/backup/reload'); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } - Future restoreBackup(String backupId) async { - var client = await getClient(); - client.put('/services/restic/backup/restore', data: {'backupId': backupId}); - close(client); + Future restoreBackup(final String backupId) async { + final Dio client = await getClient(); + try { + client.put( + '/services/restic/backup/restore', + data: {'backupId': backupId}, + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future pullConfigurationUpdate() async { - var client = await getClient(); - Response response = await client.get('/system/configuration/pull'); - close(client); - return response.statusCode == HttpStatus.ok; + Response response; + bool result = false; + + final Dio client = await getClient(); + try { + response = await client.get('/system/configuration/pull'); + result = (response.statusCode != null) + ? (response.statusCode == HttpStatus.ok) + : false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } + return result; } Future reboot() async { - var client = await getClient(); - Response response = await client.get('/system/reboot'); - close(client); - return response.statusCode == HttpStatus.ok; + Response response; + bool result = false; + + final Dio client = await getClient(); + try { + response = await client.get('/system/reboot'); + result = (response.statusCode != null) + ? (response.statusCode == HttpStatus.ok) + : false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } + return result; } Future upgrade() async { - var client = await getClient(); - Response response = await client.get('/system/configuration/upgrade'); - close(client); - return response.statusCode == HttpStatus.ok; + Response response; + bool result = false; + + final Dio client = await getClient(); + try { + response = await client.get('/system/configuration/upgrade'); + result = (response.statusCode != null) + ? (response.statusCode == HttpStatus.ok) + : false; + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } + return result; } Future getAutoUpgradeSettings() async { - var client = await getClient(); - Response response = await client.get('/system/configuration/autoUpgrade'); - close(client); - return AutoUpgradeSettings.fromJson(response.data); + Response response; + AutoUpgradeSettings settings = const AutoUpgradeSettings( + enable: false, + allowReboot: false, + ); + + final Dio client = await getClient(); + try { + response = await client.get('/system/configuration/autoUpgrade'); + if (response.data != null) { + settings = AutoUpgradeSettings.fromJson(response.data); + } + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } + return settings; } - Future updateAutoUpgradeSettings(AutoUpgradeSettings settings) async { - var client = await getClient(); - await client.put( - '/system/configuration/autoUpgrade', - data: settings.toJson(), - ); - close(client); + Future updateAutoUpgradeSettings( + final AutoUpgradeSettings settings, + ) async { + final Dio client = await getClient(); + try { + await client.put( + '/system/configuration/autoUpgrade', + data: settings.toJson(), + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } Future getServerTimezone() async { - var client = await getClient(); - Response response = await client.get('/system/configuration/timezone'); + // I am not sure how to initialize TimeZoneSettings with default value... + final Dio client = await getClient(); + final Response response = + await client.get('/system/configuration/timezone'); close(client); return TimeZoneSettings.fromString(response.data); } - Future updateServerTimezone(TimeZoneSettings settings) async { - var client = await getClient(); - await client.put( - '/system/configuration/timezone', - data: settings.toJson(), - ); - close(client); + Future updateServerTimezone(final TimeZoneSettings settings) async { + final Dio client = await getClient(); + try { + await client.put( + '/system/configuration/timezone', + data: settings.toString(), + ); + } on DioError catch (e) { + print(e.message); + } finally { + close(client); + } } - Future getDkim() async { - var client = await getClient(); - Response response = await client.get('/services/mailserver/dkim'); - close(client); + Future getDkim() async { + Response response; - // if got 404 raise exception - if (response.statusCode == HttpStatus.notFound) { + final Dio client = await getClient(); + try { + response = await client.get('/services/mailserver/dkim'); + } on DioError catch (e) { + print(e.message); + return null; + } finally { + close(client); + } + + if (response.statusCode == null) { + return null; + } + + if (response.statusCode == HttpStatus.notFound || response.data == null) { throw Exception('No DKIM key found'); } - final base64toString = utf8.fuse(base64); + if (response.statusCode != HttpStatus.ok) { + return ''; + } + + final Codec base64toString = utf8.fuse(base64); return base64toString .decode(response.data) @@ -431,6 +652,257 @@ class ServerApi extends ApiMap { .split(')')[0] .replaceAll('"', ''); } + + Future> getRecoveryTokenStatus() async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.get('/auth/recovery_token'); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: const RecoveryKeyStatus(exists: false, valid: false), + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null + ? RecoveryKeyStatus.fromJson(response.data) + : null, + ); + } + + Future> generateRecoveryToken( + final DateTime? expiration, + final int? uses, + ) async { + Response response; + + final Dio client = await getClient(); + final Map data = {}; + if (expiration != null) { + data['expiration'] = '${expiration.toIso8601String()}Z'; + print(data['expiration']); + } + if (uses != null) { + data['uses'] = uses; + } + try { + response = await client.post( + '/auth/recovery_token', + data: data, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); + } finally { + close(client); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); + } + + Future> useRecoveryToken(final DeviceToken token) async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.post( + '/auth/recovery_token/use', + data: { + 'token': token.token, + 'device': token.device, + }, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); + } + + Future> authorizeDevice(final DeviceToken token) async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.post( + '/auth/new_device/authorize', + data: { + 'token': token.token, + 'device': token.device, + }, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse(statusCode: code, data: response.data['token'] ?? ''); + } + + Future> createDeviceToken() async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.post('/auth/new_device'); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); + } + + Future> deleteDeviceToken() async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.delete('/auth/new_device'); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse(statusCode: code, data: response.data ?? ''); + } + + Future>> getApiTokens() async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.get('/auth/tokens'); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: [], + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: (response.data != null) + ? response.data + .map((final e) => ApiToken.fromJson(e)) + .toList() + : [], + ); + } + + Future> refreshCurrentApiToken() async { + Response response; + + final Dio client = await getClient(); + try { + response = await client.post('/auth/tokens'); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: '', + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + + return ApiResponse( + statusCode: code, + data: response.data != null ? response.data['token'] : '', + ); + } + + Future> deleteApiToken(final String device) async { + Response response; + final Dio client = await getClient(); + try { + response = await client.delete( + '/auth/tokens', + data: { + 'token_name': device, + }, + ); + } on DioError catch (e) { + print(e.message); + return ApiResponse( + errorMessage: e.message, + statusCode: e.response?.statusCode ?? HttpStatus.internalServerError, + data: null, + ); + } finally { + client.close(); + } + + final int code = response.statusCode ?? HttpStatus.internalServerError; + return ApiResponse(statusCode: code, data: null); + } } extension UrlServerExt on ServiceTypes { diff --git a/lib/logic/common_enum/common_enum.dart b/lib/logic/common_enum/common_enum.dart index 8568c383..3a1a4d80 100644 --- a/lib/logic/common_enum/common_enum.dart +++ b/lib/logic/common_enum/common_enum.dart @@ -1,8 +1,14 @@ import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:ionicons/ionicons.dart'; +import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; +enum LoadingStatus { + uninitialized, + refreshing, + success, + error, +} + enum InitializingSteps { setHetznerKey, setCloudFlareKey, @@ -13,6 +19,7 @@ enum InitializingSteps { startServer, checkSystemDnsAndDkimSet, } + enum Period { hour, day, month } enum ServiceTypes { @@ -126,9 +133,9 @@ extension ServiceTypesExt on ServiceTypes { case ServiceTypes.git: return BrandIcons.git; case ServiceTypes.vpn: - return Ionicons.shield_checkmark_outline; + return Icons.vpn_lock_outlined; } } - String get txt => this.toString().split('.')[1]; + String get txt => toString().split('.')[1]; } diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart deleted file mode 100644 index 69259976..00000000 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ /dev/null @@ -1,378 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/get_it/ssh.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; - -import 'app_config_repository.dart'; - -export 'package:provider/provider.dart'; - -part 'app_config_state.dart'; - -/// Initializing steps: -/// -/// The set phase. -/// 1.1. Hetzner key |setHetznerKey -/// 1.2. Cloudflare key |setCloudflareKey -/// 1.3. Backblaze Id + Key |setBackblazeKey -/// 1.4. Set Domain address |setDomain -/// 1.5. Set Root user name password |setRootUser -/// 1.6. Set Create server ans set DNS-Records |createServerAndSetDnsRecords -/// (without start) -/// -/// The check phase. -/// -/// 2.1. a. wait 60sec checkDnsAndStartServer |startServerIfDnsIsOkay -/// b. checkDns -/// c. if dns is okay start server -/// -/// 2.2. a. wait 60sec |resetServerIfServerIsOkay -/// b. checkServer -/// c. if server is ok wait 30 sec -/// d. reset server -/// -/// 2.3. a. wait 60sec |oneMoreReset() -/// d. reset server -/// -/// 2.4. a. wait 30sec |finishCheckIfServerIsOkay -/// b. checkServer -/// c. if server is okay set that fully checked - -class AppConfigCubit extends Cubit { - AppConfigCubit() : super(AppConfigEmpty()); - - final repository = AppConfigRepository(); - - Future load() async { - var state = await repository.load(); - - if (state is AppConfigFinished) { - emit(state); - } else if (state is AppConfigNotFinished) { - if (state.progress == 6) { - startServerIfDnsIsOkay(state: state, isImmediate: true); - } else if (state.progress == 7) { - resetServerIfServerIsOkay(state: state, isImmediate: true); - } else if (state.progress == 8) { - oneMoreReset(state: state, isImmediate: true); - } else if (state.progress == 9) { - finishCheckIfServerIsOkay(state: state, isImmediate: true); - } else { - emit(state); - } - } else { - throw 'wrong state'; - } - } - - void startServerIfDnsIsOkay({ - AppConfigNotFinished? state, - bool isImmediate = false, - }) async { - state = state ?? this.state as AppConfigNotFinished; - - final work = () async { - emit(TimerState(dataState: state!, isLoading: true)); - - var ip4 = state.hetznerServer!.ip4; - var domainName = state.cloudFlareDomain!.domainName; - - var matches = await repository.isDnsAddressesMatch( - domainName, ip4, state.dnsMatches); - - if (matches.values.every((value) => value)) { - var server = await repository.startServer( - state.hetznerServer!, - ); - await repository.saveServerDetails(server); - await repository.saveIsServerStarted(true); - - emit( - state.copyWith( - isServerStarted: true, - isLoading: false, - hetznerServer: server, - ), - ); - resetServerIfServerIsOkay(); - } else { - emit( - state.copyWith( - isLoading: false, - dnsMatches: matches, - ), - ); - startServerIfDnsIsOkay(); - } - }; - - if (isImmediate) { - work(); - } else { - var pauseDuration = Duration(seconds: 30); - emit(TimerState( - dataState: state, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - )); - timer = Timer(pauseDuration, work); - } - } - - void oneMoreReset({ - AppConfigNotFinished? state, - bool isImmediate = false, - }) async { - var dataState = state ?? this.state as AppConfigNotFinished; - - var work = () async { - emit(TimerState(dataState: dataState, isLoading: true)); - - var isServerWorking = await repository.isHttpServerWorking(); - - if (isServerWorking) { - var pauseDuration = Duration(seconds: 30); - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - isLoading: false, - duration: pauseDuration, - )); - timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart(); - await repository.saveIsServerResetedSecondTime(true); - await repository.saveServerDetails(hetznerServerDetails); - - emit( - dataState.copyWith( - isServerResetedSecondTime: true, - hetznerServer: hetznerServerDetails, - isLoading: false, - ), - ); - finishCheckIfServerIsOkay(); - }); - } else { - oneMoreReset(); - } - }; - if (isImmediate) { - work(); - } else { - var pauseDuration = Duration(seconds: 60); - emit( - TimerState( - dataState: dataState, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - ), - ); - timer = Timer(pauseDuration, work); - } - } - - void resetServerIfServerIsOkay({ - AppConfigNotFinished? state, - bool isImmediate = false, - }) async { - var dataState = state ?? this.state as AppConfigNotFinished; - - var work = () async { - emit(TimerState(dataState: dataState, isLoading: true)); - - var isServerWorking = await repository.isHttpServerWorking(); - - if (isServerWorking) { - var pauseDuration = Duration(seconds: 30); - emit(TimerState( - dataState: dataState, - timerStart: DateTime.now(), - isLoading: false, - duration: pauseDuration, - )); - timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart(); - await repository.saveIsServerResetedFirstTime(true); - await repository.saveServerDetails(hetznerServerDetails); - - emit( - dataState.copyWith( - isServerResetedFirstTime: true, - hetznerServer: hetznerServerDetails, - isLoading: false, - ), - ); - oneMoreReset(); - }); - } else { - resetServerIfServerIsOkay(); - } - }; - if (isImmediate) { - work(); - } else { - var pauseDuration = Duration(seconds: 60); - emit( - TimerState( - dataState: dataState, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - ), - ); - timer = Timer(pauseDuration, work); - } - } - - Timer? timer; - - void finishCheckIfServerIsOkay({ - AppConfigNotFinished? state, - bool isImmediate = false, - }) async { - state = state ?? this.state as AppConfigNotFinished; - - var work = () async { - emit(TimerState(dataState: state!, isLoading: true)); - - var isServerWorking = await repository.isHttpServerWorking(); - - if (isServerWorking) { - await repository.createDkimRecord(state.cloudFlareDomain!); - await repository.saveHasFinalChecked(true); - - emit(state.finish()); - } else { - finishCheckIfServerIsOkay(); - } - }; - if (isImmediate) { - work(); - } else { - var pauseDuration = Duration(seconds: 60); - emit( - TimerState( - dataState: state, - timerStart: DateTime.now(), - duration: pauseDuration, - isLoading: false, - ), - ); - timer = Timer(pauseDuration, work); - } - } - - void clearAppConfig() { - closeTimer(); - - repository.clearAppConfig(); - emit(AppConfigEmpty()); - } - - Future serverDelete() async { - closeTimer(); - - if (state.hetznerServer != null) { - await repository.deleteServer(state.cloudFlareDomain!); - await getIt().clear(); - } - await repository.deleteRecords(); - emit(AppConfigNotFinished( - hetznerKey: state.hetznerKey, - cloudFlareDomain: state.cloudFlareDomain, - cloudFlareKey: state.cloudFlareKey, - backblazeCredential: state.backblazeCredential, - rootUser: state.rootUser, - hetznerServer: null, - isServerStarted: false, - isServerResetedFirstTime: false, - isServerResetedSecondTime: false, - isLoading: false, - dnsMatches: null, - )); - } - - void setHetznerKey(String hetznerKey) async { - await repository.saveHetznerKey(hetznerKey); - emit((state as AppConfigNotFinished).copyWith(hetznerKey: hetznerKey)); - } - - void setCloudflareKey(String cloudFlareKey) async { - await repository.saveCloudFlareKey(cloudFlareKey); - emit( - (state as AppConfigNotFinished).copyWith(cloudFlareKey: cloudFlareKey)); - } - - void setBackblazeKey(String keyId, String applicationKey) async { - var backblazeCredential = BackblazeCredential( - keyId: keyId, - applicationKey: applicationKey, - ); - await repository.saveBackblazeKey(backblazeCredential); - emit((state as AppConfigNotFinished) - .copyWith(backblazeCredential: backblazeCredential)); - } - - void setDomain(CloudFlareDomain cloudFlareDomain) async { - await repository.saveDomain(cloudFlareDomain); - emit((state as AppConfigNotFinished) - .copyWith(cloudFlareDomain: cloudFlareDomain)); - } - - void setRootUser(User rootUser) async { - await repository.saveRootUser(rootUser); - emit((state as AppConfigNotFinished).copyWith(rootUser: rootUser)); - } - - void createServerAndSetDnsRecords() async { - AppConfigNotFinished _stateCopy = state as AppConfigNotFinished; - var onSuccess = (HetznerServerDetails serverDetails) async { - await repository.createDnsRecords( - serverDetails.ip4, - state.cloudFlareDomain!, - ); - - emit((state as AppConfigNotFinished).copyWith( - isLoading: false, - hetznerServer: serverDetails, - )); - startServerIfDnsIsOkay(); - }; - - var onCancel = - () => emit((state as AppConfigNotFinished).copyWith(isLoading: false)); - - try { - emit((state as AppConfigNotFinished).copyWith(isLoading: true)); - await repository.createServer( - state.rootUser!, - state.cloudFlareDomain!.domainName, - state.cloudFlareKey!, - state.backblazeCredential!, - onCancel: onCancel, - onSuccess: onSuccess, - ); - } catch (e) { - emit(_stateCopy); - } - } - - close() { - closeTimer(); - return super.close(); - } - - void closeTimer() { - if (timer != null && timer!.isActive) { - timer!.cancel(); - } - } -} diff --git a/lib/logic/cubit/app_config/app_config_repository.dart b/lib/logic/cubit/app_config/app_config_repository.dart deleted file mode 100644 index 76a63b25..00000000 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'package:basic_utils/basic_utils.dart'; -import 'package:dio/dio.dart'; -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/cloudflare.dart'; -import 'package:selfprivacy/logic/api_maps/hetzner.dart'; -import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; -import 'package:selfprivacy/logic/models/message.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; -import 'package:selfprivacy/logic/models/user.dart'; -import 'package:selfprivacy/ui/components/action_button/action_button.dart'; -import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; - -import 'app_config_cubit.dart'; - -class AppConfigRepository { - Box box = Hive.box(BNames.appConfig); - - Future load() async { - late AppConfigState res; - if (box.get(BNames.hasFinalChecked, defaultValue: false)) { - res = AppConfigFinished( - hetznerKey: getIt().hetznerKey!, - cloudFlareKey: getIt().cloudFlareKey!, - cloudFlareDomain: getIt().cloudFlareDomain!, - backblazeCredential: getIt().backblazeCredential!, - hetznerServer: getIt().hetznerServer!, - rootUser: box.get(BNames.rootUser), - isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), - isServerResetedFirstTime: - box.get(BNames.isServerResetedFirstTime, defaultValue: false), - isServerResetedSecondTime: - box.get(BNames.isServerResetedSecondTime, defaultValue: false), - ); - } else { - res = AppConfigNotFinished( - hetznerKey: getIt().hetznerKey, - cloudFlareKey: getIt().cloudFlareKey, - cloudFlareDomain: getIt().cloudFlareDomain, - backblazeCredential: getIt().backblazeCredential, - hetznerServer: getIt().hetznerServer, - rootUser: box.get(BNames.rootUser), - isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), - isServerResetedFirstTime: - box.get(BNames.isServerResetedFirstTime, defaultValue: false), - isServerResetedSecondTime: - box.get(BNames.isServerResetedSecondTime, defaultValue: false), - isLoading: box.get(BNames.isLoading, defaultValue: false), - dnsMatches: null, - ); - } - - return res; - } - - void clearAppConfig() { - box.clear(); - } - - Future startServer( - HetznerServerDetails hetznerServer, - ) async { - var hetznerApi = HetznerApi(); - var serverDetails = await hetznerApi.powerOn(); - - return serverDetails; - } - - Future> isDnsAddressesMatch(String? domainName, String? ip4, - Map? skippedMatches) async { - var addresses = [ - '$domainName', - 'api.$domainName', - 'cloud.$domainName', - 'meet.$domainName', - 'password.$domainName' - ]; - - var matches = {}; - - for (var address in addresses) { - if (skippedMatches != null && skippedMatches[address] == true) { - matches[address] = true; - continue; - } - var lookupRecordRes = await DnsUtils.lookupRecord( - address, - RRecordType.A, - provider: DnsApiProvider.CLOUDFLARE, - ); - getIt.get().addMessage( - Message( - text: - 'DnsLookup: address: $address, $RRecordType, provider: CLOUDFLARE, ip4: $ip4', - ), - ); - getIt.get().addMessage( - Message( - text: - 'DnsLookup: ${lookupRecordRes == null ? 'empty' : (lookupRecordRes[0].data != ip4 ? 'wrong ip4' : 'right ip4')}', - ), - ); - if (lookupRecordRes == null || - lookupRecordRes.isEmpty || - lookupRecordRes[0].data != ip4) { - matches[address] = false; - } else { - matches[address] = true; - } - } - - return matches; - } - - Future createServer( - User rootUser, - String domainName, - String cloudFlareKey, - BackblazeCredential backblazeCredential, { - required void Function() onCancel, - required Future Function(HetznerServerDetails serverDetails) - onSuccess, - }) async { - var hetznerApi = HetznerApi(); - late HetznerDataBase dataBase; - - try { - dataBase = await hetznerApi.createVolume(); - - var serverDetails = await hetznerApi.createServer( - cloudFlareKey: cloudFlareKey, - rootUser: rootUser, - domainName: domainName, - dataBase: dataBase, - ); - saveServerDetails(serverDetails); - onSuccess(serverDetails); - } on DioError catch (e) { - if (e.response!.data['error']['code'] == 'uniqueness_error') { - var nav = getIt.get(); - nav.showPopUpDialog( - BrandAlert( - title: 'modals.1'.tr(), - contentText: 'modals.2'.tr(), - actions: [ - ActionButton( - text: 'basis.delete'.tr(), - isRed: true, - onPressed: () async { - await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: domainName); - - var serverDetails = await hetznerApi.createServer( - cloudFlareKey: cloudFlareKey, - rootUser: rootUser, - domainName: domainName, - dataBase: dataBase, - ); - - await saveServerDetails(serverDetails); - onSuccess(serverDetails); - }, - ), - ActionButton( - text: 'basis.cancel'.tr(), - onPressed: () { - onCancel(); - }, - ), - ], - ), - ); - } - } - } - - Future createDnsRecords( - String ip4, - CloudFlareDomain cloudFlareDomain, - ) async { - var cloudflareApi = CloudflareApi(); - - await cloudflareApi.removeSimilarRecords( - ip4: ip4, - cloudFlareDomain: cloudFlareDomain, - ); - - await cloudflareApi.createMultipleDnsRecords( - ip4: ip4, - cloudFlareDomain: cloudFlareDomain, - ); - - await HetznerApi().createReverseDns( - ip4: ip4, - domainName: cloudFlareDomain.domainName, - ); - } - - Future createDkimRecord(CloudFlareDomain cloudFlareDomain) async { - var cloudflareApi = CloudflareApi(); - var api = ServerApi(); - - var dkimRecordString = await api.getDkim(); - - await cloudflareApi.setDkim(dkimRecordString, cloudFlareDomain); - } - - Future isHttpServerWorking() async { - var api = ServerApi(); - var isHttpServerWorking = await api.isHttpServerWorking(); - try { - await api.getDkim(); - } catch (e) { - return false; - } - return isHttpServerWorking; - } - - Future restart() async { - var hetznerApi = HetznerApi(); - return await hetznerApi.reset(); - } - - Future powerOn() async { - var hetznerApi = HetznerApi(); - return await hetznerApi.powerOn(); - } - - Future saveServerDetails(HetznerServerDetails serverDetails) async { - await getIt().storeServerDetails(serverDetails); - } - - Future saveHetznerKey(String key) async { - print('saved'); - await getIt().storeHetznerKey(key); - } - - Future saveBackblazeKey(BackblazeCredential backblazeCredential) async { - await getIt().storeBackblazeCredential(backblazeCredential); - } - - Future saveCloudFlareKey(String key) async { - await getIt().storeCloudFlareKey(key); - } - - Future saveDomain(CloudFlareDomain cloudFlareDomain) async { - await getIt().storeCloudFlareDomain(cloudFlareDomain); - } - - Future saveIsServerStarted(bool value) async { - await box.put(BNames.isServerStarted, value); - } - - Future saveIsServerResetedFirstTime(bool value) async { - await box.put(BNames.isServerResetedFirstTime, value); - } - - Future saveIsServerResetedSecondTime(bool value) async { - await box.put(BNames.isServerResetedSecondTime, value); - } - - Future saveRootUser(User rootUser) async { - await box.put(BNames.rootUser, rootUser); - } - - Future saveHasFinalChecked(bool value) async { - await box.put(BNames.hasFinalChecked, value); - } - - Future deleteServer(CloudFlareDomain cloudFlareDomain) async { - var hetznerApi = HetznerApi(); - var cloudFlare = CloudflareApi(); - - await hetznerApi.deleteSelfprivacyServerAndAllVolumes( - domainName: cloudFlareDomain.domainName, - ); - - await box.put(BNames.hasFinalChecked, false); - await box.put(BNames.isServerStarted, false); - await box.put(BNames.isServerResetedFirstTime, false); - await box.put(BNames.isServerResetedSecondTime, false); - await box.put(BNames.isLoading, false); - await box.put(BNames.hetznerServer, null); - - await cloudFlare.removeSimilarRecords(cloudFlareDomain: cloudFlareDomain); - } - - Future deleteRecords() async { - await box.deleteAll([ - BNames.hetznerServer, - BNames.isServerStarted, - BNames.isServerResetedFirstTime, - BNames.isServerResetedSecondTime, - BNames.hasFinalChecked, - BNames.isLoading, - ]); - getIt().init(); - } -} diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart deleted file mode 100644 index 0608d903..00000000 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ /dev/null @@ -1,242 +0,0 @@ -part of 'app_config_cubit.dart'; - -abstract class AppConfigState extends Equatable { - const AppConfigState({ - required this.hetznerKey, - required this.cloudFlareKey, - required this.backblazeCredential, - required this.cloudFlareDomain, - required this.rootUser, - required this.hetznerServer, - required this.isServerStarted, - required this.isServerResetedFirstTime, - required this.isServerResetedSecondTime, - }); - - @override - List get props => [ - hetznerKey, - cloudFlareKey, - backblazeCredential, - cloudFlareDomain, - rootUser, - hetznerServer, - isServerStarted, - isServerResetedFirstTime, - ]; - - final String? hetznerKey; - final String? cloudFlareKey; - final BackblazeCredential? backblazeCredential; - final CloudFlareDomain? cloudFlareDomain; - final User? rootUser; - final HetznerServerDetails? hetznerServer; - final bool isServerStarted; - final bool isServerResetedFirstTime; - final bool isServerResetedSecondTime; - - bool get isHetznerFilled => hetznerKey != null; - bool get isCloudFlareFilled => cloudFlareKey != null; - bool get isBackblazeFilled => backblazeCredential != null; - bool get isDomainFilled => cloudFlareDomain != null; - bool get isUserFilled => rootUser != null; - bool get isServerCreated => hetznerServer != null; - - bool get isFullyInitilized => _fulfilementList.every((el) => el!); - int get progress => _fulfilementList.where((el) => el!).length; - - int get porgressBar { - if (progress < 6) { - return progress; - } else if (progress < 10) { - return 6; - } else { - return 7; - } - } - - List get _fulfilementList { - var res = [ - isHetznerFilled, - isCloudFlareFilled, - isBackblazeFilled, - isDomainFilled, - isUserFilled, - isServerCreated, - isServerStarted, - isServerResetedFirstTime, - isServerResetedSecondTime, - ]; - - return res; - } -} - -class TimerState extends AppConfigNotFinished { - TimerState({ - required this.dataState, - this.timerStart, - this.duration, - required bool isLoading, - }) : super( - hetznerKey: dataState.hetznerKey, - cloudFlareKey: dataState.cloudFlareKey, - backblazeCredential: dataState.backblazeCredential, - cloudFlareDomain: dataState.cloudFlareDomain, - rootUser: dataState.rootUser, - hetznerServer: dataState.hetznerServer, - isServerStarted: dataState.isServerStarted, - isServerResetedFirstTime: dataState.isServerResetedFirstTime, - isServerResetedSecondTime: dataState.isServerResetedSecondTime, - isLoading: isLoading, - dnsMatches: dataState.dnsMatches, - ); - - final AppConfigNotFinished dataState; - final DateTime? timerStart; - final Duration? duration; - - @override - List get props => [ - dataState, - timerStart, - duration, - ]; -} - -class AppConfigNotFinished extends AppConfigState { - final bool isLoading; - final Map? dnsMatches; - - AppConfigNotFinished({ - String? hetznerKey, - String? cloudFlareKey, - BackblazeCredential? backblazeCredential, - CloudFlareDomain? cloudFlareDomain, - User? rootUser, - HetznerServerDetails? hetznerServer, - required bool isServerStarted, - required bool isServerResetedFirstTime, - required bool isServerResetedSecondTime, - required this.isLoading, - required this.dnsMatches, - }) : super( - hetznerKey: hetznerKey, - cloudFlareKey: cloudFlareKey, - backblazeCredential: backblazeCredential, - cloudFlareDomain: cloudFlareDomain, - rootUser: rootUser, - hetznerServer: hetznerServer, - isServerStarted: isServerStarted, - isServerResetedFirstTime: isServerResetedFirstTime, - isServerResetedSecondTime: isServerResetedSecondTime, - ); - - @override - List get props => [ - hetznerKey, - cloudFlareKey, - backblazeCredential, - cloudFlareDomain, - rootUser, - hetznerServer, - isServerStarted, - isServerResetedFirstTime, - isLoading, - dnsMatches, - ]; - - AppConfigNotFinished copyWith({ - String? hetznerKey, - String? cloudFlareKey, - BackblazeCredential? backblazeCredential, - CloudFlareDomain? cloudFlareDomain, - User? rootUser, - HetznerServerDetails? hetznerServer, - bool? isServerStarted, - bool? isServerResetedFirstTime, - bool? isServerResetedSecondTime, - bool? isLoading, - Map? dnsMatches, - }) => - AppConfigNotFinished( - hetznerKey: hetznerKey ?? this.hetznerKey, - cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, - backblazeCredential: backblazeCredential ?? this.backblazeCredential, - cloudFlareDomain: cloudFlareDomain ?? this.cloudFlareDomain, - rootUser: rootUser ?? this.rootUser, - hetznerServer: hetznerServer ?? this.hetznerServer, - isServerStarted: isServerStarted ?? this.isServerStarted, - isServerResetedFirstTime: - isServerResetedFirstTime ?? this.isServerResetedFirstTime, - isServerResetedSecondTime: - isServerResetedSecondTime ?? this.isServerResetedSecondTime, - isLoading: isLoading ?? this.isLoading, - dnsMatches: dnsMatches ?? this.dnsMatches, - ); - - AppConfigFinished finish() => AppConfigFinished( - hetznerKey: hetznerKey!, - cloudFlareKey: cloudFlareKey!, - backblazeCredential: backblazeCredential!, - cloudFlareDomain: cloudFlareDomain!, - rootUser: rootUser!, - hetznerServer: hetznerServer!, - isServerStarted: isServerStarted, - isServerResetedFirstTime: isServerResetedFirstTime, - isServerResetedSecondTime: isServerResetedSecondTime, - ); -} - -class AppConfigEmpty extends AppConfigNotFinished { - AppConfigEmpty() - : super( - hetznerKey: null, - cloudFlareKey: null, - backblazeCredential: null, - cloudFlareDomain: null, - rootUser: null, - hetznerServer: null, - isServerStarted: false, - isServerResetedFirstTime: false, - isServerResetedSecondTime: false, - isLoading: false, - dnsMatches: null, - ); -} - -class AppConfigFinished extends AppConfigState { - const AppConfigFinished({ - required String hetznerKey, - required String cloudFlareKey, - required BackblazeCredential backblazeCredential, - required CloudFlareDomain cloudFlareDomain, - required User rootUser, - required HetznerServerDetails hetznerServer, - required bool isServerStarted, - required bool isServerResetedFirstTime, - required bool isServerResetedSecondTime, - }) : super( - hetznerKey: hetznerKey, - cloudFlareKey: cloudFlareKey, - backblazeCredential: backblazeCredential, - cloudFlareDomain: cloudFlareDomain, - rootUser: rootUser, - hetznerServer: hetznerServer, - isServerStarted: isServerStarted, - isServerResetedFirstTime: isServerResetedFirstTime, - isServerResetedSecondTime: isServerResetedSecondTime, - ); - - @override - List get props => [ - hetznerKey, - cloudFlareKey, - backblazeCredential, - cloudFlareDomain, - rootUser, - hetznerServer, - isServerStarted, - isServerResetedFirstTime, - ]; -} diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart index 70d1af8f..096a4d4f 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart @@ -1,32 +1,33 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; -export 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +export 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; part 'authentication_dependend_state.dart'; -abstract class AppConfigDependendCubit - extends Cubit { - AppConfigDependendCubit( - this.appConfigCubit, - T initState, +abstract class ServerInstallationDependendCubit< + T extends ServerInstallationDependendState> extends Cubit { + ServerInstallationDependendCubit( + this.serverInstallationCubit, + final T initState, ) : super(initState) { - authCubitSubscription = appConfigCubit.stream.listen(checkAuthStatus); - checkAuthStatus(appConfigCubit.state); + authCubitSubscription = + serverInstallationCubit.stream.listen(checkAuthStatus); + checkAuthStatus(serverInstallationCubit.state); } - void checkAuthStatus(AppConfigState state) { - if (state is AppConfigFinished) { + void checkAuthStatus(final ServerInstallationState state) { + if (state is ServerInstallationFinished) { load(); - } else if (state is AppConfigEmpty) { + } else if (state is ServerInstallationEmpty) { clear(); } } late StreamSubscription authCubitSubscription; - final AppConfigCubit appConfigCubit; + final ServerInstallationCubit serverInstallationCubit; void load(); void clear(); diff --git a/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart b/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart index 43339c85..668d63d0 100644 --- a/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart +++ b/lib/logic/cubit/app_config_dependent/authentication_dependend_state.dart @@ -1,5 +1,5 @@ part of 'authentication_dependend_cubit.dart'; -abstract class AppConfigDependendState extends Equatable { - const AppConfigDependendState(); +abstract class ServerInstallationDependendState extends Equatable { + const ServerInstallationDependendState(); } diff --git a/lib/logic/cubit/app_settings/app_settings_cubit.dart b/lib/logic/cubit/app_settings/app_settings_cubit.dart index 5dcf348e..06b46730 100644 --- a/lib/logic/cubit/app_settings/app_settings_cubit.dart +++ b/lib/logic/cubit/app_settings/app_settings_cubit.dart @@ -1,41 +1,44 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; + export 'package:provider/provider.dart'; part 'app_settings_state.dart'; class AppSettingsCubit extends Cubit { AppSettingsCubit({ - required bool isDarkModeOn, - required bool isOnbordingShowing, + required final bool isDarkModeOn, + required final bool isOnboardingShowing, }) : super( AppSettingsState( isDarkModeOn: isDarkModeOn, - isOnbordingShowing: isOnbordingShowing, + isOnboardingShowing: isOnboardingShowing, ), ); - Box box = Hive.box(BNames.appSettings); + Box box = Hive.box(BNames.appSettingsBox); void load() { - bool? isDarkModeOn = box.get(BNames.isDarkModeOn); - bool? isOnbordingShowing = box.get(BNames.isOnbordingShowing); - emit(state.copyWith( - isDarkModeOn: isDarkModeOn, - isOnbordingShowing: isOnbordingShowing, - )); + final bool? isDarkModeOn = box.get(BNames.isDarkModeOn); + final bool? isOnboardingShowing = box.get(BNames.isOnboardingShowing); + emit( + state.copyWith( + isDarkModeOn: isDarkModeOn, + isOnboardingShowing: isOnboardingShowing, + ), + ); } - void updateDarkMode({required bool isDarkModeOn}) { + void updateDarkMode({required final bool isDarkModeOn}) { box.put(BNames.isDarkModeOn, isDarkModeOn); emit(state.copyWith(isDarkModeOn: isDarkModeOn)); } void turnOffOnboarding() { - box.put(BNames.isOnbordingShowing, false); + box.put(BNames.isOnboardingShowing, false); - emit(state.copyWith(isOnbordingShowing: false)); + emit(state.copyWith(isOnboardingShowing: false)); } } diff --git a/lib/logic/cubit/app_settings/app_settings_state.dart b/lib/logic/cubit/app_settings/app_settings_state.dart index e1dae427..92da9667 100644 --- a/lib/logic/cubit/app_settings/app_settings_state.dart +++ b/lib/logic/cubit/app_settings/app_settings_state.dart @@ -3,18 +3,21 @@ part of 'app_settings_cubit.dart'; class AppSettingsState extends Equatable { const AppSettingsState({ required this.isDarkModeOn, - required this.isOnbordingShowing, + required this.isOnboardingShowing, }); final bool isDarkModeOn; - final bool isOnbordingShowing; + final bool isOnboardingShowing; - AppSettingsState copyWith({isDarkModeOn, isOnbordingShowing}) => + AppSettingsState copyWith({ + final bool? isDarkModeOn, + final bool? isOnboardingShowing, + }) => AppSettingsState( isDarkModeOn: isDarkModeOn ?? this.isDarkModeOn, - isOnbordingShowing: isOnbordingShowing ?? this.isOnbordingShowing, + isOnboardingShowing: isOnboardingShowing ?? this.isOnboardingShowing, ); @override - List get props => [isDarkModeOn, isOnbordingShowing]; + List get props => [isDarkModeOn, isOnboardingShowing]; } diff --git a/lib/logic/cubit/backups/backups_cubit.dart b/lib/logic/cubit/backups/backups_cubit.dart index e79978c2..63cdfb3e 100644 --- a/lib/logic/cubit/backups/backups_cubit.dart +++ b/lib/logic/cubit/backups/backups_cubit.dart @@ -5,78 +5,95 @@ import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/json/backup.dart'; part 'backups_state.dart'; -class BackupsCubit extends AppConfigDependendCubit { - BackupsCubit(AppConfigCubit appConfigCubit) - : super(appConfigCubit, BackupsState(preventActions: true)); +class BackupsCubit extends ServerInstallationDependendCubit { + BackupsCubit(final ServerInstallationCubit serverInstallationCubit) + : super( + serverInstallationCubit, + const BackupsState(preventActions: true), + ); - final api = ServerApi(); - final backblaze = BackblazeApi(); + final ServerApi api = ServerApi(); + final BackblazeApi backblaze = BackblazeApi(); + @override Future load() async { - if (appConfigCubit.state is AppConfigFinished) { - final bucket = getIt().backblazeBucket; + if (serverInstallationCubit.state is ServerInstallationFinished) { + final BackblazeBucket? bucket = getIt().backblazeBucket; if (bucket == null) { - emit(BackupsState( - isInitialized: false, preventActions: false, refreshing: false)); + emit( + const BackupsState( + isInitialized: false, + preventActions: false, + refreshing: false, + ), + ); } else { - final status = await api.getBackupStatus(); + final BackupStatus status = await api.getBackupStatus(); switch (status.status) { case BackupStatusEnum.noKey: case BackupStatusEnum.notInitialized: - emit(BackupsState( - backups: [], - isInitialized: true, - preventActions: false, - progress: 0, - status: status.status, - refreshing: false, - )); + emit( + BackupsState( + backups: const [], + isInitialized: true, + preventActions: false, + progress: 0, + status: status.status, + refreshing: false, + ), + ); break; case BackupStatusEnum.initializing: - emit(BackupsState( - backups: [], - isInitialized: true, - preventActions: false, - progress: 0, - status: status.status, - refreshTimer: Duration(seconds: 10), - refreshing: false, - )); + emit( + BackupsState( + backups: const [], + isInitialized: true, + preventActions: false, + progress: 0, + status: status.status, + refreshTimer: const Duration(seconds: 10), + refreshing: false, + ), + ); break; case BackupStatusEnum.initialized: case BackupStatusEnum.error: - final backups = await api.getBackups(); - emit(BackupsState( - backups: backups, - isInitialized: true, - preventActions: false, - progress: status.progress, - status: status.status, - error: status.errorMessage ?? '', - refreshing: false, - )); + final List backups = await api.getBackups(); + emit( + BackupsState( + backups: backups, + isInitialized: true, + preventActions: false, + progress: status.progress, + status: status.status, + error: status.errorMessage ?? '', + refreshing: false, + ), + ); break; case BackupStatusEnum.backingUp: case BackupStatusEnum.restoring: - final backups = await api.getBackups(); - emit(BackupsState( - backups: backups, - isInitialized: true, - preventActions: true, - progress: status.progress, - status: status.status, - error: status.errorMessage ?? '', - refreshTimer: Duration(seconds: 5), - refreshing: false, - )); + final List backups = await api.getBackups(); + emit( + BackupsState( + backups: backups, + isInitialized: true, + preventActions: true, + progress: status.progress, + status: status.status, + error: status.errorMessage ?? '', + refreshTimer: const Duration(seconds: 5), + refreshing: false, + ), + ); break; default: - emit(BackupsState()); + emit(const BackupsState()); } Timer(state.refreshTimer, () => updateBackups(useTimer: true)); } @@ -85,22 +102,23 @@ class BackupsCubit extends AppConfigDependendCubit { Future createBucket() async { emit(state.copyWith(preventActions: true)); - final domain = appConfigCubit.state.cloudFlareDomain!.domainName + final String domain = serverInstallationCubit.state.serverDomain!.domainName .replaceAll(RegExp(r'[^a-zA-Z0-9]'), '-'); - final serverId = appConfigCubit.state.hetznerServer!.id; - var bucketName = 'selfprivacy-$domain-$serverId'; + final int serverId = serverInstallationCubit.state.serverDetails!.id; + String bucketName = 'selfprivacy-$domain-$serverId'; // If bucket name is too long, shorten it if (bucketName.length > 49) { bucketName = bucketName.substring(0, 49); } - final bucketId = await backblaze.createBucket(bucketName); + final String bucketId = await backblaze.createBucket(bucketName); - final key = await backblaze.createKey(bucketId); - final bucket = BackblazeBucket( - bucketId: bucketId, - bucketName: bucketName, - applicationKey: key.applicationKey, - applicationKeyId: key.applicationKeyId); + final BackblazeApplicationKey key = await backblaze.createKey(bucketId); + final BackblazeBucket bucket = BackblazeBucket( + bucketId: bucketId, + bucketName: bucketName, + applicationKey: key.applicationKey, + applicationKeyId: key.applicationKeyId, + ); await getIt().storeBackblazeBucket(bucket); await api.uploadBackblazeConfig(bucket); @@ -111,7 +129,7 @@ class BackupsCubit extends AppConfigDependendCubit { Future reuploadKey() async { emit(state.copyWith(preventActions: true)); - final bucket = getIt().backblazeBucket; + final BackblazeBucket? bucket = getIt().backblazeBucket; if (bucket == null) { emit(state.copyWith(isInitialized: false)); } else { @@ -121,32 +139,35 @@ class BackupsCubit extends AppConfigDependendCubit { } } - Duration refreshTimeFromState(BackupStatusEnum status) { + Duration refreshTimeFromState(final BackupStatusEnum status) { switch (status) { case BackupStatusEnum.backingUp: case BackupStatusEnum.restoring: - return Duration(seconds: 5); + return const Duration(seconds: 5); case BackupStatusEnum.initializing: - return Duration(seconds: 10); + return const Duration(seconds: 10); default: - return Duration(seconds: 60); + return const Duration(seconds: 60); } } - Future updateBackups({bool useTimer = false}) async { + Future updateBackups({final bool useTimer = false}) async { emit(state.copyWith(refreshing: true)); - final backups = await api.getBackups(); - final status = await api.getBackupStatus(); - emit(state.copyWith( - backups: backups, - progress: status.progress, - status: status.status, - error: status.errorMessage, - refreshTimer: refreshTimeFromState(status.status), - refreshing: false, - )); - if (useTimer) + final List backups = await api.getBackups(); + final BackupStatus status = await api.getBackupStatus(); + emit( + state.copyWith( + backups: backups, + progress: status.progress, + status: status.status, + error: status.errorMessage, + refreshTimer: refreshTimeFromState(status.status), + refreshing: false, + ), + ); + if (useTimer) { Timer(state.refreshTimer, () => updateBackups(useTimer: true)); + } } Future forceUpdateBackups() async { @@ -164,7 +185,7 @@ class BackupsCubit extends AppConfigDependendCubit { emit(state.copyWith(preventActions: false)); } - Future restoreBackup(String backupId) async { + Future restoreBackup(final String backupId) async { emit(state.copyWith(preventActions: true)); await api.restoreBackup(backupId); emit(state.copyWith(preventActions: false)); @@ -172,6 +193,6 @@ class BackupsCubit extends AppConfigDependendCubit { @override void clear() async { - emit(BackupsState()); + emit(const BackupsState()); } } diff --git a/lib/logic/cubit/backups/backups_state.dart b/lib/logic/cubit/backups/backups_state.dart index 6b0bc5e3..33ec52c8 100644 --- a/lib/logic/cubit/backups/backups_state.dart +++ b/lib/logic/cubit/backups/backups_state.dart @@ -1,13 +1,13 @@ part of 'backups_cubit.dart'; -class BackupsState extends AppConfigDependendState { +class BackupsState extends ServerInstallationDependendState { const BackupsState({ this.isInitialized = false, this.backups = const [], this.progress = 0.0, this.status = BackupStatusEnum.noKey, this.preventActions = true, - this.error = "", + this.error = '', this.refreshTimer = const Duration(seconds: 60), this.refreshing = true, }); @@ -34,14 +34,14 @@ class BackupsState extends AppConfigDependendState { ]; BackupsState copyWith({ - bool? isInitialized, - List? backups, - double? progress, - BackupStatusEnum? status, - bool? preventActions, - String? error, - Duration? refreshTimer, - bool? refreshing, + final bool? isInitialized, + final List? backups, + final double? progress, + final BackupStatusEnum? status, + final bool? preventActions, + final String? error, + final Duration? refreshTimer, + final bool? refreshing, }) => BackupsState( isInitialized: isInitialized ?? this.isInitialized, diff --git a/lib/logic/cubit/devices/devices_cubit.dart b/lib/logic/cubit/devices/devices_cubit.dart new file mode 100644 index 00000000..f0380635 --- /dev/null +++ b/lib/logic/cubit/devices/devices_cubit.dart @@ -0,0 +1,78 @@ +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/api_maps/server.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 { + ApiDevicesCubit(final ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, const ApiDevicesState.initial()); + + final ServerApi api = ServerApi(); + + @override + void load() async { + if (serverInstallationCubit.state is ServerInstallationFinished) { + final List? devices = await _getApiTokens(); + if (devices != null) { + emit(ApiDevicesState(devices, LoadingStatus.success)); + } else { + emit(const ApiDevicesState([], LoadingStatus.error)); + } + } + } + + Future refresh() async { + emit(const ApiDevicesState([], LoadingStatus.refreshing)); + final List? devices = await _getApiTokens(); + if (devices != null) { + emit(ApiDevicesState(devices, LoadingStatus.success)); + } else { + emit(const ApiDevicesState([], LoadingStatus.error)); + } + } + + Future?> _getApiTokens() async { + final ApiResponse> response = await api.getApiTokens(); + if (response.isSuccess) { + return response.data; + } else { + return null; + } + } + + Future deleteDevice(final ApiToken device) async { + final ApiResponse response = await api.deleteApiToken(device.name); + if (response.isSuccess) { + emit( + ApiDevicesState( + state.devices.where((final d) => d.name != device.name).toList(), + LoadingStatus.success, + ), + ); + } else { + getIt() + .showSnackBar(response.errorMessage ?? 'Error deleting device'); + } + } + + Future getNewDeviceKey() async { + final ApiResponse response = await api.createDeviceToken(); + if (response.isSuccess) { + return response.data; + } else { + getIt().showSnackBar( + response.errorMessage ?? 'Error getting new device key', + ); + return null; + } + } + + @override + void clear() { + emit(const ApiDevicesState.initial()); + } +} diff --git a/lib/logic/cubit/devices/devices_state.dart b/lib/logic/cubit/devices/devices_state.dart new file mode 100644 index 00000000..86fd53c2 --- /dev/null +++ b/lib/logic/cubit/devices/devices_state.dart @@ -0,0 +1,34 @@ +part of 'devices_cubit.dart'; + +class ApiDevicesState extends ServerInstallationDependendState { + const ApiDevicesState(this._devices, this.status); + + const ApiDevicesState.initial() : this(const [], LoadingStatus.uninitialized); + final List _devices; + final LoadingStatus status; + + List get devices => _devices; + ApiToken get thisDevice => _devices.firstWhere( + (final device) => device.isCaller, + orElse: () => ApiToken( + name: 'Error fetching device', + isCaller: true, + date: DateTime.now(), + ), + ); + + List get otherDevices => + _devices.where((final device) => !device.isCaller).toList(); + + ApiDevicesState copyWith({ + final List? devices, + final LoadingStatus? status, + }) => + ApiDevicesState( + devices ?? _devices, + status ?? this.status, + ); + + @override + List get props => [_devices]; +} diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart index 227ac227..0590b065 100644 --- a/lib/logic/cubit/dns_records/dns_records_cubit.dart +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -1,91 +1,109 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; -import 'package:selfprivacy/logic/models/dns_records.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; +import 'package:selfprivacy/logic/models/json/dns_records.dart'; -import '../../api_maps/cloudflare.dart'; -import '../../api_maps/server.dart'; +import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; part 'dns_records_state.dart'; -class DnsRecordsCubit extends AppConfigDependendCubit { - DnsRecordsCubit(AppConfigCubit appConfigCubit) - : super(appConfigCubit, - DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); +class DnsRecordsCubit + extends ServerInstallationDependendCubit { + DnsRecordsCubit(final ServerInstallationCubit serverInstallationCubit) + : super( + serverInstallationCubit, + const DnsRecordsState(dnsState: DnsRecordsStatus.refreshing), + ); - final api = ServerApi(); - final cloudflare = CloudflareApi(); + final ServerApi api = ServerApi(); + final CloudflareApi cloudflare = CloudflareApi(); + @override Future load() async { - emit(DnsRecordsState( + emit( + DnsRecordsState( dnsState: DnsRecordsStatus.refreshing, dnsRecords: _getDesiredDnsRecords( - appConfigCubit.state.cloudFlareDomain?.domainName, "", ""))); + serverInstallationCubit.state.serverDomain?.domainName, + '', + '', + ), + ), + ); print('Loading DNS status'); - if (appConfigCubit.state is AppConfigFinished) { - final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; - final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + if (serverInstallationCubit.state is ServerInstallationFinished) { + final ServerDomain? domain = serverInstallationCubit.state.serverDomain; + final String? ipAddress = + serverInstallationCubit.state.serverDetails?.ip4; if (domain != null && ipAddress != null) { final List records = await cloudflare.getDnsRecords(cloudFlareDomain: domain); - final dkimPublicKey = await api.getDkim(); - final desiredRecords = + final String? dkimPublicKey = await api.getDkim(); + final List desiredRecords = _getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey); - List foundRecords = []; - for (final record in desiredRecords) { + final List foundRecords = []; + for (final DesiredDnsRecord record in desiredRecords) { if (record.description == 'providers.domain.record_description.dkim') { - final foundRecord = records.firstWhere( - (r) => r.name == record.name && r.type == record.type, - orElse: () => DnsRecord( - name: record.name, - type: record.type, - content: '', - ttl: 800, - proxied: false)); + final DnsRecord foundRecord = records.firstWhere( + (final r) => r.name == record.name && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false, + ), + ); // remove all spaces and tabulators from // the foundRecord.content and the record.content // to compare them - final foundContent = + final String? foundContent = foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); - final content = record.content.replaceAll(RegExp(r'\s+'), ''); + final String content = + record.content.replaceAll(RegExp(r'\s+'), ''); if (foundContent == content) { foundRecords.add(record.copyWith(isSatisfied: true)); } else { foundRecords.add(record.copyWith(isSatisfied: false)); } } else { - if (records.any((r) => - r.name == record.name && - r.type == record.type && - r.content == record.content)) { + if (records.any( + (final r) => + r.name == record.name && + r.type == record.type && + r.content == record.content, + )) { foundRecords.add(record.copyWith(isSatisfied: true)); } else { foundRecords.add(record.copyWith(isSatisfied: false)); } } } - emit(DnsRecordsState( - dnsRecords: foundRecords, - dnsState: foundRecords.any((r) => r.isSatisfied == false) - ? DnsRecordsStatus.error - : DnsRecordsStatus.good, - )); + emit( + DnsRecordsState( + dnsRecords: foundRecords, + dnsState: foundRecords.any((final r) => r.isSatisfied == false) + ? DnsRecordsStatus.error + : DnsRecordsStatus.good, + ), + ); } else { - emit(DnsRecordsState()); + emit(const DnsRecordsState()); } } } @override - void onChange(Change change) { + void onChange(final Change change) { // print(change); super.onChange(change); } @override Future clear() async { - emit(DnsRecordsState(dnsState: DnsRecordsStatus.error)); + emit(const DnsRecordsState(dnsState: DnsRecordsStatus.error)); } Future refresh() async { @@ -95,18 +113,23 @@ class DnsRecordsCubit extends AppConfigDependendCubit { Future fix() async { emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); - final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; - final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; - final dkimPublicKey = await api.getDkim(); + final ServerDomain? domain = serverInstallationCubit.state.serverDomain; + final String? ipAddress = serverInstallationCubit.state.serverDetails?.ip4; + final String? dkimPublicKey = await api.getDkim(); await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); await cloudflare.createMultipleDnsRecords( - cloudFlareDomain: domain, ip4: ipAddress); - await cloudflare.setDkim(dkimPublicKey, domain); + cloudFlareDomain: domain, + ip4: ipAddress, + ); + await cloudflare.setDkim(dkimPublicKey ?? '', domain); await load(); } List _getDesiredDnsRecords( - String? domainName, String? ipAddress, String? dkimPublicKey) { + final String? domainName, + final String? ipAddress, + final String? dkimPublicKey, + ) { if (domainName == null || ipAddress == null || dkimPublicKey == null) { return []; } diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart index dc594e74..4b39d014 100644 --- a/lib/logic/cubit/dns_records/dns_records_state.dart +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -13,7 +13,7 @@ enum DnsRecordsCategory { other, } -class DnsRecordsState extends AppConfigDependendState { +class DnsRecordsState extends ServerInstallationDependendState { const DnsRecordsState({ this.dnsState = DnsRecordsStatus.uninitialized, this.dnsRecords = const [], @@ -29,21 +29,20 @@ class DnsRecordsState extends AppConfigDependendState { ]; DnsRecordsState copyWith({ - DnsRecordsStatus? dnsState, - List? dnsRecords, - }) { - return DnsRecordsState( - dnsState: dnsState ?? this.dnsState, - dnsRecords: dnsRecords ?? this.dnsRecords, - ); - } + final DnsRecordsStatus? dnsState, + final List? dnsRecords, + }) => + DnsRecordsState( + dnsState: dnsState ?? this.dnsState, + dnsRecords: dnsRecords ?? this.dnsRecords, + ); } class DesiredDnsRecord { const DesiredDnsRecord({ required this.name, - this.type = "A", required this.content, + this.type = 'A', this.description = '', this.category = DnsRecordsCategory.services, this.isSatisfied = false, @@ -57,20 +56,19 @@ class DesiredDnsRecord { final bool isSatisfied; DesiredDnsRecord copyWith({ - String? name, - String? type, - String? content, - String? description, - DnsRecordsCategory? category, - bool? isSatisfied, - }) { - return DesiredDnsRecord( - name: name ?? this.name, - type: type ?? this.type, - content: content ?? this.content, - description: description ?? this.description, - category: category ?? this.category, - isSatisfied: isSatisfied ?? this.isSatisfied, - ); - } + final String? name, + final String? type, + final String? content, + final String? description, + final DnsRecordsCategory? category, + final bool? isSatisfied, + }) => + DesiredDnsRecord( + name: name ?? this.name, + type: type ?? this.type, + content: content ?? this.content, + description: description ?? this.description, + category: category ?? this.category, + isSatisfied: isSatisfied ?? this.isSatisfied, + ); } diff --git a/lib/logic/cubit/forms/factories/field_cubit_factory.dart b/lib/logic/cubit/forms/factories/field_cubit_factory.dart index d3255a5f..62067cea 100644 --- a/lib/logic/cubit/forms/factories/field_cubit_factory.dart +++ b/lib/logic/cubit/forms/factories/field_cubit_factory.dart @@ -16,21 +16,26 @@ class FieldCubitFactory { /// - Must not be a reserved root login /// - Must be unique FieldCubit createUserLoginField() { - final userAllowedRegExp = RegExp(r"^[a-z_][a-z0-9_]+$"); - const userMaxLength = 31; + final RegExp userAllowedRegExp = RegExp(r'^[a-z_][a-z0-9_]+$'); + const int userMaxLength = 31; return FieldCubit( initalValue: '', validations: [ ValidationModel( - (s) => s.toLowerCase() == 'root', 'validations.root_name'.tr()), + (final String s) => s.toLowerCase() == 'root', + 'validations.root_name'.tr(), + ), ValidationModel( - (login) => context.read().state.isLoginRegistered(login), + (final String login) => + context.read().state.isLoginRegistered(login), 'validations.user_already_exist'.tr(), ), RequiredStringValidation('validations.required'.tr()), LengthStringLongerValidation(userMaxLength), - ValidationModel((s) => !userAllowedRegExp.hasMatch(s), - 'validations.invalid_format'.tr()), + ValidationModel( + (final String s) => !userAllowedRegExp.hasMatch(s), + 'validations.invalid_format'.tr(), + ), ], ); } @@ -40,17 +45,25 @@ class FieldCubitFactory { /// - Must fail on the regural expression of invalid matches: [\n\r\s]+ /// - Must not be empty FieldCubit createUserPasswordField() { - var passwordForbiddenRegExp = RegExp(r"[\n\r\s]+"); + final RegExp passwordForbiddenRegExp = RegExp(r'[\n\r\s]+'); return FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - (password) => passwordForbiddenRegExp.hasMatch(password), - 'validations.invalid_format'.tr()), + passwordForbiddenRegExp.hasMatch, + 'validations.invalid_format'.tr(), + ), ], ); } + FieldCubit createRequiredStringField() => FieldCubit( + initalValue: '', + validations: [ + RequiredStringValidation('validations.required'.tr()), + ], + ); + final BuildContext context; } diff --git a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart similarity index 72% rename from lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart index d8777fa8..4769286d 100644 --- a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart @@ -1,12 +1,12 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/backblaze.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; import 'package:easy_localization/easy_localization.dart'; class BackblazeFormCubit extends FormCubit { - BackblazeFormCubit(this.initializingCubit) { + BackblazeFormCubit(this.serverInstallationCubit) { //var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); keyId = FieldCubit( initalValue: '', @@ -27,13 +27,13 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr onSubmit() async { - initializingCubit.setBackblazeKey( + serverInstallationCubit.setBackblazeKey( keyId.state.value, applicationKey.state.value, ); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit keyId; late final FieldCubit applicationKey; @@ -41,16 +41,17 @@ class BackblazeFormCubit extends FormCubit { @override FutureOr asyncValidation() async { late bool isKeyValid; - BackblazeApi apiClient = BackblazeApi(isWithToken: false); + final BackblazeApi apiClient = BackblazeApi(isWithToken: false); try { - String encodedApiKey = encodedBackblazeKey( + final String encodedApiKey = encodedBackblazeKey( keyId.state.value, applicationKey.state.value, ); isKeyValid = await apiClient.isValid(encodedApiKey); } catch (e) { addError(e); + isKeyValid = false; } if (!isKeyValid) { diff --git a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart similarity index 73% rename from lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart index d811843b..01d26835 100644 --- a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart @@ -3,18 +3,20 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { CloudFlareFormCubit(this.initializingCubit) { - var regExp = RegExp(r"\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); + final RegExp regExp = RegExp(r'\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); apiKey = FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), + regExp.hasMatch, + 'validations.key_format'.tr(), + ), LengthStringNotEqualValidation(40) ], ); @@ -27,14 +29,14 @@ class CloudFlareFormCubit extends FormCubit { initializingCubit.setCloudflareKey(apiKey.state.value); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit initializingCubit; late final FieldCubit apiKey; @override FutureOr asyncValidation() async { late bool isKeyValid; - CloudflareApi apiClient = CloudflareApi(isWithToken: false); + final CloudflareApi apiClient = CloudflareApi(isWithToken: false); try { isKeyValid = await apiClient.isValid(apiKey.state.value); @@ -48,9 +50,4 @@ class CloudFlareFormCubit extends FormCubit { } return true; } - - @override - Future close() async { - return super.close(); - } } diff --git a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart similarity index 59% rename from lib/logic/cubit/forms/initializing/domain_cloudflare.dart rename to lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart index 78a244c8..89b50a62 100644 --- a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/setup/initializing/domain_cloudflare.dart @@ -1,18 +1,18 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/api_maps/cloudflare.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class DomainSetupCubit extends Cubit { - DomainSetupCubit(this.initializingCubit) : super(Initial()); + DomainSetupCubit(this.serverInstallationCubit) : super(Initial()); - final AppConfigCubit initializingCubit; + final ServerInstallationCubit serverInstallationCubit; Future load() async { emit(Loading(LoadingTypes.loadingDomain)); - var api = CloudflareApi(); + final CloudflareApi api = CloudflareApi(); - var list = await api.domainList(); + final List list = await api.domainList(); if (list.isEmpty) { emit(Empty()); } else if (list.length == 1) { @@ -23,25 +23,24 @@ class DomainSetupCubit extends Cubit { } @override - Future close() { - return super.close(); - } + Future close() => super.close(); Future saveDomain() async { assert(state is Loaded, 'wrong state'); - var domainName = (state as Loaded).domain; - var api = CloudflareApi(); + final String domainName = (state as Loaded).domain; + final CloudflareApi api = CloudflareApi(); emit(Loading(LoadingTypes.saving)); - var zoneId = await api.getZoneId(domainName); + final String zoneId = await api.getZoneId(domainName); - var domain = CloudFlareDomain( + final ServerDomain domain = ServerDomain( domainName: domainName, zoneId: zoneId, + provider: DnsProvider.cloudflare, ); - initializingCubit.setDomain(domain); + serverInstallationCubit.setDomain(domain); emit(DomainSet()); } } @@ -62,9 +61,8 @@ class Loading extends DomainSetupState { enum LoadingTypes { loadingDomain, saving } class Loaded extends DomainSetupState { - final String domain; - Loaded(this.domain); + final String domain; } class DomainSet extends DomainSetupState {} diff --git a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart similarity index 66% rename from lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart index ce3e5aa9..b8f47e10 100644 --- a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart @@ -3,18 +3,20 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/api_maps/hetzner.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class HetznerFormCubit extends FormCubit { - HetznerFormCubit(this.initializingCubit) { - var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); + HetznerFormCubit(this.serverInstallationCubit) { + final RegExp regExp = RegExp(r'\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]'); apiKey = FieldCubit( initalValue: '', validations: [ RequiredStringValidation('validations.required'.tr()), ValidationModel( - (s) => regExp.hasMatch(s), 'validations.key_format'.tr()), + regExp.hasMatch, + 'validations.key_format'.tr(), + ), LengthStringNotEqualValidation(64) ], ); @@ -24,17 +26,17 @@ class HetznerFormCubit extends FormCubit { @override FutureOr onSubmit() async { - initializingCubit.setHetznerKey(apiKey.state.value); + serverInstallationCubit.setHetznerKey(apiKey.state.value); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit apiKey; @override FutureOr asyncValidation() async { late bool isKeyValid; - HetznerApi apiClient = HetznerApi(isWithToken: false); + final HetznerApi apiClient = HetznerApi(isWithToken: false); try { isKeyValid = await apiClient.isValid(apiKey.state.value); diff --git a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart similarity index 65% rename from lib/logic/cubit/forms/initializing/root_user_form_cubit.dart rename to lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart index 102d7ac7..b3cf606f 100644 --- a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart @@ -1,13 +1,15 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; class RootUserFormCubit extends FormCubit { RootUserFormCubit( - this.initializingCubit, final FieldCubitFactory fieldFactory) { + this.serverInstallationCubit, + final FieldCubitFactory fieldFactory, + ) { userName = fieldFactory.createUserLoginField(); password = fieldFactory.createUserPasswordField(); @@ -18,14 +20,14 @@ class RootUserFormCubit extends FormCubit { @override FutureOr onSubmit() async { - var user = User( + final User user = User( login: userName.state.value, password: password.state.value, ); - initializingCubit.setRootUser(user); + serverInstallationCubit.setRootUser(user); } - final AppConfigCubit initializingCubit; + final ServerInstallationCubit serverInstallationCubit; late final FieldCubit userName; late final FieldCubit password; diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart new file mode 100644 index 00000000..ad93871c --- /dev/null +++ b/lib/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; + +class RecoveryDeviceFormCubit extends FormCubit { + RecoveryDeviceFormCubit( + this.installationCubit, + final FieldCubitFactory fieldFactory, + this.recoveryMethod, + ) { + tokenField = fieldFactory.createRequiredStringField(); + + super.addFields([tokenField]); + } + + @override + FutureOr onSubmit() async { + late final String token; + // Trim spaces and make lowercase + if (recoveryMethod == ServerRecoveryMethods.recoveryKey || + recoveryMethod == ServerRecoveryMethods.newDeviceKey) { + token = tokenField.state.value.trim().toLowerCase(); + } else { + token = tokenField.state.value.trim(); + } + + installationCubit.tryToRecover(token, recoveryMethod); + } + + final ServerInstallationCubit installationCubit; + late final FieldCubit tokenField; + final ServerRecoveryMethods recoveryMethod; +} diff --git a/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart new file mode 100644 index 00000000..664b87b8 --- /dev/null +++ b/lib/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; + +class RecoveryDomainFormCubit extends FormCubit { + RecoveryDomainFormCubit( + this.initializingCubit, + final FieldCubitFactory fieldFactory, + ) { + serverDomainField = fieldFactory.createRequiredStringField(); + + super.addFields([serverDomainField]); + } + + @override + FutureOr onSubmit() async { + initializingCubit + .submitDomainForAccessRecovery(serverDomainField.state.value); + } + + @override + FutureOr asyncValidation() async { + final ServerApi api = ServerApi( + hasLogger: false, + isWithToken: false, + overrideDomain: serverDomainField.state.value, + ); + + // API version doesn't require access token, + // so if the entered domain is indeed valid + // and the server by it is reachable, we will + // be able to confirm the input + + final bool domainValid = await api.getApiVersion() != null; + if (!domainValid) { + serverDomainField.setError('recovering.domain_recover_error'.tr()); + } + + return domainValid; + } + + FutureOr setCustomError(final String error) { + serverDomainField.setError(error); + } + + final ServerInstallationCubit initializingCubit; + late final FieldCubit serverDomainField; +} diff --git a/lib/logic/cubit/forms/user/ssh_form_cubit.dart b/lib/logic/cubit/forms/user/ssh_form_cubit.dart index d51cac6b..ba38a642 100644 --- a/lib/logic/cubit/forms/user/ssh_form_cubit.dart +++ b/lib/logic/cubit/forms/user/ssh_form_cubit.dart @@ -4,29 +4,34 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; class SshFormCubit extends FormCubit { SshFormCubit({ required this.jobsCubit, required this.user, }) { - var keyRegExp = RegExp( - r"^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$"); + final RegExp keyRegExp = RegExp( + r'^(ssh-rsa AAAAB3NzaC1yc2|ssh-ed25519 AAAAC3NzaC1lZDI1NTE5)[0-9A-Za-z+/]+[=]{0,3}( .*)?$', + ); key = FieldCubit( initalValue: '', validations: [ ValidationModel( - (newKey) => user.sshKeys.any((key) => key == newKey), + (final String newKey) => + user.sshKeys.any((final String key) => key == newKey), 'validations.key_already_exists'.tr(), ), RequiredStringValidation('validations.required'.tr()), - ValidationModel((s) { - print(s); - print(keyRegExp.hasMatch(s)); - return !keyRegExp.hasMatch(s); - }, 'validations.invalid_format'.tr()), + ValidationModel( + (final String s) { + print(s); + print(keyRegExp.hasMatch(s)); + return !keyRegExp.hasMatch(s); + }, + 'validations.invalid_format'.tr(), + ), ], ); diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index b65cfb47..a385befb 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -4,29 +4,30 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/utils/password_generator.dart'; class UserFormCubit extends FormCubit { UserFormCubit({ required this.jobsCubit, - required FieldCubitFactory fieldFactory, - User? user, + required final FieldCubitFactory fieldFactory, + final User? user, }) { - var isEdit = user != null; + final bool isEdit = user != null; login = fieldFactory.createUserLoginField(); - login.setValue(isEdit ? user!.login : ''); + login.setValue(isEdit ? user.login : ''); password = fieldFactory.createUserPasswordField(); password.setValue( - isEdit ? (user?.password ?? '') : StringGenerators.userPassword()); + isEdit ? (user.password ?? '') : StringGenerators.userPassword(), + ); super.addFields([login, password]); } @override FutureOr onSubmit() { - var user = User( + final User user = User( login: login.state.value, password: password.state.value, ); diff --git a/lib/logic/cubit/forms/validations/validations.dart b/lib/logic/cubit/forms/validations/validations.dart index 91a8f75c..f233bded 100644 --- a/lib/logic/cubit/forms/validations/validations.dart +++ b/lib/logic/cubit/forms/validations/validations.dart @@ -2,27 +2,31 @@ import 'package:cubit_form/cubit_form.dart'; import 'package:easy_localization/easy_localization.dart'; abstract class LengthStringValidation extends ValidationModel { - LengthStringValidation(bool Function(String) predicate, String errorMessage) - : super(predicate, errorMessage); + LengthStringValidation(super.predicate, super.errorMessage); @override - String? check(String value) { - var length = value.length; - var errorMessage = this.errorMassage.replaceAll("[]", length.toString()); - return test(value) ? errorMessage : null; + String? check(final String val) { + final int length = val.length; + final String errorMessage = + errorMassage.replaceAll('[]', length.toString()); + return test(val) ? errorMessage : null; } } class LengthStringNotEqualValidation extends LengthStringValidation { /// String must be equal to [length] - LengthStringNotEqualValidation(int length) - : super((n) => n.length != length, - 'validations.length_not_equal'.tr(args: [length.toString()])); + LengthStringNotEqualValidation(final int length) + : super( + (final n) => n.length != length, + 'validations.length_not_equal'.tr(args: [length.toString()]), + ); } class LengthStringLongerValidation extends LengthStringValidation { /// String must be shorter than or equal to [length] - LengthStringLongerValidation(int length) - : super((n) => n.length > length, - 'validations.length_longer'.tr(args: [length.toString()])); + LengthStringLongerValidation(final int length) + : super( + (final n) => n.length > length, + 'validations.length_longer'.tr(args: [length.toString()]), + ); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart index 3795c828..aaae36c5 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart @@ -1,22 +1,23 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; -import 'hetzner_metrics_repository.dart'; +import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart'; part 'hetzner_metrics_state.dart'; class HetznerMetricsCubit extends Cubit { - HetznerMetricsCubit() : super(HetznerMetricsLoading(Period.day)); + HetznerMetricsCubit() : super(const HetznerMetricsLoading(Period.day)); - final repository = HetznerMetricsRepository(); + final HetznerMetricsRepository repository = HetznerMetricsRepository(); Timer? timer; - close() { + @override + Future close() { closeTimer(); return super.close(); } @@ -27,7 +28,7 @@ class HetznerMetricsCubit extends Cubit { } } - void changePeriod(Period period) async { + void changePeriod(final Period period) async { closeTimer(); emit(HetznerMetricsLoading(period)); load(period); @@ -37,8 +38,8 @@ class HetznerMetricsCubit extends Cubit { load(state.period); } - void load(Period period) async { - var newState = await repository.getMetrics(period); + void load(final Period period) async { + final HetznerMetricsLoaded newState = await repository.getMetrics(period); timer = Timer( Duration(seconds: newState.stepInSeconds.toInt()), () => load(newState.period), diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart index 11ce2bc1..de7f3d43 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_repository.dart @@ -2,40 +2,40 @@ import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; -import 'hetzner_metrics_cubit.dart'; +import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; class HetznerMetricsRepository { - Future getMetrics(Period period) async { - var end = DateTime.now(); + Future getMetrics(final Period period) async { + final DateTime end = DateTime.now(); DateTime start; switch (period) { case Period.hour: - start = end.subtract(Duration(hours: 1)); + start = end.subtract(const Duration(hours: 1)); break; case Period.day: - start = end.subtract(Duration(days: 1)); + start = end.subtract(const Duration(days: 1)); break; case Period.month: - start = end.subtract(Duration(days: 15)); + start = end.subtract(const Duration(days: 15)); break; } - var api = HetznerApi(hasLogger: true); + final HetznerApi api = HetznerApi(hasLogger: true); - var results = await Future.wait([ + final List> results = await Future.wait([ api.getMetrics(start, end, 'cpu'), api.getMetrics(start, end, 'network'), ]); - var cpuMetricsData = results[0]["metrics"]; - var networkMetricsData = results[1]["metrics"]; + final cpuMetricsData = results[0]['metrics']; + final networkMetricsData = results[1]['metrics']; return HetznerMetricsLoaded( period: period, start: start, end: end, - stepInSeconds: cpuMetricsData["step"], + stepInSeconds: cpuMetricsData['step'], cpu: timeSeriesSerializer(cpuMetricsData, 'cpu'), ppsIn: timeSeriesSerializer(networkMetricsData, 'network.0.pps.in'), ppsOut: timeSeriesSerializer(networkMetricsData, 'network.0.pps.out'), @@ -50,7 +50,11 @@ class HetznerMetricsRepository { } List timeSeriesSerializer( - Map json, String type) { - List list = json["time_series"][type]["values"]; - return list.map((el) => TimeSeriesData(el[0], double.parse(el[1]))).toList(); + final Map json, + final String type, +) { + final List list = json['time_series'][type]['values']; + return list + .map((final el) => TimeSeriesData(el[0], double.parse(el[1]))) + .toList(); } diff --git a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart index bbd3f7d1..b6204db9 100644 --- a/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart +++ b/lib/logic/cubit/hetzner_metrics/hetzner_metrics_state.dart @@ -7,7 +7,8 @@ abstract class HetznerMetricsState extends Equatable { } class HetznerMetricsLoading extends HetznerMetricsState { - HetznerMetricsLoading(this.period); + const HetznerMetricsLoading(this.period); + @override final Period period; @override @@ -15,7 +16,7 @@ class HetznerMetricsLoading extends HetznerMetricsState { } class HetznerMetricsLoaded extends HetznerMetricsState { - HetznerMetricsLoaded({ + const HetznerMetricsLoaded({ required this.period, required this.start, required this.end, @@ -27,6 +28,7 @@ class HetznerMetricsLoaded extends HetznerMetricsState { required this.bandwidthOut, }); + @override final Period period; final DateTime start; final DateTime end; diff --git a/lib/logic/cubit/jobs/jobs_cubit.dart b/lib/logic/cubit/jobs/jobs_cubit.dart index f2ce57d1..6de64677 100644 --- a/lib/logic/cubit/jobs/jobs_cubit.dart +++ b/lib/logic/cubit/jobs/jobs_cubit.dart @@ -17,12 +17,12 @@ class JobsCubit extends Cubit { required this.servicesCubit, }) : super(JobsStateEmpty()); - final api = ServerApi(); + final ServerApi api = ServerApi(); final UsersCubit usersCubit; final ServicesCubit servicesCubit; - void addJob(Job job) { - var newJobsList = []; + void addJob(final Job job) { + final List newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } @@ -31,21 +31,22 @@ class JobsCubit extends Cubit { emit(JobsStateWithJobs(newJobsList)); } - void removeJob(String id) { - final newState = (state as JobsStateWithJobs).removeById(id); + void removeJob(final String id) { + final JobsState newState = (state as JobsStateWithJobs).removeById(id); emit(newState); } - void createOrRemoveServiceToggleJob(ToggleJob job) { - var newJobsList = []; + void createOrRemoveServiceToggleJob(final ToggleJob job) { + final List newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } - var needToRemoveJob = - newJobsList.any((el) => el is ServiceToggleJob && el.type == job.type); + final bool needToRemoveJob = newJobsList + .any((final el) => el is ServiceToggleJob && el.type == job.type); if (needToRemoveJob) { - var removingJob = newJobsList - .firstWhere(((el) => el is ServiceToggleJob && el.type == job.type)); + final Job removingJob = newJobsList.firstWhere( + (final el) => el is ServiceToggleJob && el.type == job.type, + ); removeJob(removingJob.id); } else { newJobsList.add(job); @@ -54,12 +55,13 @@ class JobsCubit extends Cubit { } } - void createShhJobIfNotExist(CreateSSHKeyJob job) { - var newJobsList = []; + void createShhJobIfNotExist(final CreateSSHKeyJob job) { + final List newJobsList = []; if (state is JobsStateWithJobs) { newJobsList.addAll((state as JobsStateWithJobs).jobList); } - var isExistInJobList = newJobsList.any((el) => el is CreateSSHKeyJob); + final bool isExistInJobList = + newJobsList.any((final el) => el is CreateSSHKeyJob); if (!isExistInJobList) { newJobsList.add(job); getIt().showSnackBar('jobs.jobAdded'.tr()); @@ -69,7 +71,7 @@ class JobsCubit extends Cubit { Future rebootServer() async { emit(JobsStateLoading()); - final isSuccessful = await api.reboot(); + final bool isSuccessful = await api.reboot(); if (isSuccessful) { getIt().showSnackBar('jobs.rebootSuccess'.tr()); } else { @@ -80,8 +82,8 @@ class JobsCubit extends Cubit { Future upgradeServer() async { emit(JobsStateLoading()); - final isPullSuccessful = await api.pullConfigurationUpdate(); - final isSuccessful = await api.upgrade(); + final bool isPullSuccessful = await api.pullConfigurationUpdate(); + final bool isSuccessful = await api.upgrade(); if (isSuccessful) { if (!isPullSuccessful) { getIt().showSnackBar('jobs.configPullFailed'.tr()); @@ -96,10 +98,10 @@ class JobsCubit extends Cubit { Future applyAll() async { if (state is JobsStateWithJobs) { - var jobs = (state as JobsStateWithJobs).jobList; + final List jobs = (state as JobsStateWithJobs).jobList; emit(JobsStateLoading()); - var hasServiceJobs = false; - for (var job in jobs) { + bool hasServiceJobs = false; + for (final Job job in jobs) { if (job is CreateUserJob) { await usersCubit.createUser(job.user); } diff --git a/lib/logic/cubit/jobs/jobs_state.dart b/lib/logic/cubit/jobs/jobs_state.dart index 972f4b3d..dbcf968e 100644 --- a/lib/logic/cubit/jobs/jobs_state.dart +++ b/lib/logic/cubit/jobs/jobs_state.dart @@ -13,8 +13,9 @@ class JobsStateWithJobs extends JobsState { JobsStateWithJobs(this.jobList); final List jobList; - JobsState removeById(String id) { - var newJobsList = jobList.where((element) => element.id != id).toList(); + JobsState removeById(final String id) { + final List newJobsList = + jobList.where((final element) => element.id != id).toList(); if (newJobsList.isEmpty) { return JobsStateEmpty(); diff --git a/lib/logic/cubit/providers/providers_cubit.dart b/lib/logic/cubit/providers/providers_cubit.dart index 5f92e225..d3ce60b9 100644 --- a/lib/logic/cubit/providers/providers_cubit.dart +++ b/lib/logic/cubit/providers/providers_cubit.dart @@ -1,4 +1,4 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/logic/models/provider.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; @@ -11,8 +11,9 @@ part 'providers_state.dart'; class ProvidersCubit extends Cubit { ProvidersCubit() : super(InitialProviderState()); - void connect(ProviderModel provider) { - var newState = state.updateElement(provider, StateType.stable); + void connect(final ProviderModel provider) { + final ProvidersState newState = + state.updateElement(provider, StateType.stable); emit(newState); } } diff --git a/lib/logic/cubit/providers/providers_state.dart b/lib/logic/cubit/providers/providers_state.dart index 8297699d..04146b5d 100644 --- a/lib/logic/cubit/providers/providers_state.dart +++ b/lib/logic/cubit/providers/providers_state.dart @@ -5,18 +5,23 @@ class ProvidersState extends Equatable { final List all; - ProvidersState updateElement(ProviderModel provider, StateType newState) { - var newList = [...all]; - var index = newList.indexOf(provider); + ProvidersState updateElement( + final ProviderModel provider, + final StateType newState, + ) { + final List newList = [...all]; + final int index = newList.indexOf(provider); newList[index] = provider.updateState(newState); return ProvidersState(newList); } - List get connected => - all.where((service) => service.state != StateType.uninitialized).toList(); + List get connected => all + .where((final service) => service.state != StateType.uninitialized) + .toList(); - List get uninitialized => - all.where((service) => service.state == StateType.uninitialized).toList(); + List get uninitialized => all + .where((final service) => service.state == StateType.uninitialized) + .toList(); bool get isFullyInitialized => uninitialized.isEmpty; @@ -29,7 +34,7 @@ class InitialProviderState extends ProvidersState { : super( ProviderType.values .map( - (type) => ProviderModel( + (final type) => ProviderModel( state: StateType.uninitialized, type: type, ), diff --git a/lib/logic/cubit/recovery_key/recovery_key_cubit.dart b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart new file mode 100644 index 00000000..abd7b2fa --- /dev/null +++ b/lib/logic/cubit/recovery_key/recovery_key_cubit.dart @@ -0,0 +1,79 @@ +import 'package:selfprivacy/logic/api_maps/server.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 { + 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 _getRecoveryKeyStatus() async { + final ApiResponse response = + await api.getRecoveryTokenStatus(); + if (response.isSuccess) { + return response.data; + } else { + return null; + } + } + + Future 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 generateRecoveryKey({ + final DateTime? expirationDate, + final int? numberOfUses, + }) async { + final ApiResponse response = + await api.generateRecoveryToken(expirationDate, numberOfUses); + if (response.isSuccess) { + refresh(); + return response.data; + } else { + throw GenerationError(response.errorMessage ?? 'Unknown error'); + } + } + + @override + void clear() { + emit(state.copyWith(loadingStatus: LoadingStatus.uninitialized)); + } +} + +class GenerationError extends Error { + GenerationError(this.message); + final String message; +} diff --git a/lib/logic/cubit/recovery_key/recovery_key_state.dart b/lib/logic/cubit/recovery_key/recovery_key_state.dart new file mode 100644 index 00000000..b35ae9a3 --- /dev/null +++ b/lib/logic/cubit/recovery_key/recovery_key_state.dart @@ -0,0 +1,39 @@ +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 get props => [_status, loadingStatus]; + + RecoveryKeyState copyWith({ + final RecoveryKeyStatus? status, + final LoadingStatus? loadingStatus, + }) => + RecoveryKeyState( + status ?? _status, + loadingStatus ?? this.loadingStatus, + ); +} diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart index b4969037..613069b0 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -1,9 +1,9 @@ -import 'package:bloc/bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_repository.dart'; -import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; part 'server_detailed_info_state.dart'; @@ -14,16 +14,18 @@ class ServerDetailsCubit extends Cubit { ServerDetailsRepository repository = ServerDetailsRepository(); void check() async { - var isReadyToCheck = getIt().hetznerServer != null; + final bool isReadyToCheck = getIt().serverDetails != null; if (isReadyToCheck) { emit(ServerDetailsLoading()); - var data = await repository.load(); - emit(Loaded( - serverInfo: data.hetznerServerInfo, - autoUpgradeSettings: data.autoUpgradeSettings, - serverTimezone: data.serverTimezone, - checkTime: DateTime.now(), - )); + final ServerDetailsRepositoryDto data = await repository.load(); + emit( + Loaded( + serverInfo: data.hetznerServerInfo, + autoUpgradeSettings: data.autoUpgradeSettings, + serverTimezone: data.serverTimezone, + checkTime: DateTime.now(), + ), + ); } else { emit(ServerDetailsNotReady()); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart index a5d6c07e..97dc6292 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -1,16 +1,16 @@ import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/logic/models/auto_upgrade_settings.dart'; -import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/json/auto_upgrade_settings.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; import 'package:selfprivacy/logic/models/timezone_settings.dart'; class ServerDetailsRepository { - var hetznerAPi = HetznerApi(); - var selfprivacyServer = ServerApi(); + HetznerApi hetznerAPi = HetznerApi(); + ServerApi selfprivacyServer = ServerApi(); - Future<_ServerDetailsRepositoryDto> load() async { + Future load() async { print('load'); - return _ServerDetailsRepositoryDto( + return ServerDetailsRepositoryDto( autoUpgradeSettings: await selfprivacyServer.getAutoUpgradeSettings(), hetznerServerInfo: await hetznerAPi.getInfo(), serverTimezone: await selfprivacyServer.getServerTimezone(), @@ -18,16 +18,15 @@ class ServerDetailsRepository { } } -class _ServerDetailsRepositoryDto { +class ServerDetailsRepositoryDto { + ServerDetailsRepositoryDto({ + required this.hetznerServerInfo, + required this.serverTimezone, + required this.autoUpgradeSettings, + }); final HetznerServerInfo hetznerServerInfo; final TimeZoneSettings serverTimezone; final AutoUpgradeSettings autoUpgradeSettings; - - _ServerDetailsRepositoryDto({ - required this.hetznerServerInfo, - required this.serverTimezone, - required this.autoUpgradeSettings, - }); } diff --git a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart index b4524751..ef226c1e 100644 --- a/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -16,6 +16,12 @@ class ServerDetailsNotReady extends ServerDetailsState {} class Loading extends ServerDetailsState {} class Loaded extends ServerDetailsState { + const Loaded({ + required this.serverInfo, + required this.serverTimezone, + required this.autoUpgradeSettings, + required this.checkTime, + }); final HetznerServerInfo serverInfo; final TimeZoneSettings serverTimezone; @@ -23,13 +29,6 @@ class Loaded extends ServerDetailsState { final AutoUpgradeSettings autoUpgradeSettings; final DateTime checkTime; - Loaded({ - required this.serverInfo, - required this.serverTimezone, - required this.autoUpgradeSettings, - required this.checkTime, - }); - @override List get props => [ serverInfo, diff --git a/lib/logic/cubit/server_installation/server_installation_cubit.dart b/lib/logic/cubit/server_installation/server_installation_cubit.dart new file mode 100644 index 00000000..ef83104d --- /dev/null +++ b/lib/logic/cubit/server_installation/server_installation_cubit.dart @@ -0,0 +1,626 @@ +import 'dart:async'; + +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:equatable/equatable.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.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/server_basic_info.dart'; + +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_repository.dart'; + +export 'package:provider/provider.dart'; + +part '../server_installation/server_installation_state.dart'; + +class ServerInstallationCubit extends Cubit { + ServerInstallationCubit() : super(const ServerInstallationEmpty()); + + final ServerInstallationRepository repository = + ServerInstallationRepository(); + + Timer? timer; + + Future load() async { + final ServerInstallationState state = await repository.load(); + + if (state is ServerInstallationFinished) { + emit(state); + } else if (state is ServerInstallationNotFinished) { + if (state.progress == ServerSetupProgress.serverCreated) { + startServerIfDnsIsOkay(state: state); + } else if (state.progress == ServerSetupProgress.serverStarted) { + resetServerIfServerIsOkay(state: state); + } else if (state.progress == ServerSetupProgress.serverResetedFirstTime) { + oneMoreReset(state: state); + } else if (state.progress == + ServerSetupProgress.serverResetedSecondTime) { + finishCheckIfServerIsOkay(state: state); + } else { + emit(state); + } + } else if (state is ServerInstallationRecovery) { + emit(state); + } else { + throw 'wrong state'; + } + } + + void setHetznerKey(final String hetznerKey) async { + await repository.saveHetznerKey(hetznerKey); + + if (state is ServerInstallationRecovery) { + emit( + (state as ServerInstallationRecovery).copyWith( + hetznerKey: hetznerKey, + currentStep: RecoveryStep.serverSelection, + ), + ); + return; + } + + emit( + (state as ServerInstallationNotFinished).copyWith(hetznerKey: hetznerKey), + ); + } + + void setCloudflareKey(final String cloudFlareKey) async { + if (state is ServerInstallationRecovery) { + setAndValidateCloudflareToken(cloudFlareKey); + return; + } + await repository.saveCloudFlareKey(cloudFlareKey); + emit( + (state as ServerInstallationNotFinished) + .copyWith(cloudFlareKey: cloudFlareKey), + ); + } + + void setBackblazeKey(final String keyId, final String applicationKey) async { + final BackblazeCredential backblazeCredential = BackblazeCredential( + keyId: keyId, + applicationKey: applicationKey, + ); + await repository.saveBackblazeKey(backblazeCredential); + if (state is ServerInstallationRecovery) { + finishRecoveryProcess(backblazeCredential); + return; + } + emit( + (state as ServerInstallationNotFinished) + .copyWith(backblazeCredential: backblazeCredential), + ); + } + + void setDomain(final ServerDomain serverDomain) async { + await repository.saveDomain(serverDomain); + emit( + (state as ServerInstallationNotFinished) + .copyWith(serverDomain: serverDomain), + ); + } + + void setRootUser(final User rootUser) async { + await repository.saveRootUser(rootUser); + emit((state as ServerInstallationNotFinished).copyWith(rootUser: rootUser)); + } + + void createServerAndSetDnsRecords() async { + final ServerInstallationNotFinished stateCopy = + state as ServerInstallationNotFinished; + void onCancel() => emit( + (state as ServerInstallationNotFinished).copyWith(isLoading: false), + ); + + Future onSuccess(final ServerHostingDetails serverDetails) async { + await repository.createDnsRecords( + serverDetails.ip4, + state.serverDomain!, + onCancel: onCancel, + ); + + emit( + (state as ServerInstallationNotFinished).copyWith( + isLoading: false, + serverDetails: serverDetails, + ), + ); + runDelayed(startServerIfDnsIsOkay, const Duration(seconds: 30), null); + } + + try { + emit((state as ServerInstallationNotFinished).copyWith(isLoading: true)); + await repository.createServer( + state.rootUser!, + state.serverDomain!.domainName, + state.cloudFlareKey!, + state.backblazeCredential!, + onCancel: onCancel, + onSuccess: onSuccess, + ); + } catch (e) { + emit(stateCopy); + } + } + + void startServerIfDnsIsOkay({ + final ServerInstallationNotFinished? state, + }) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; + + emit(TimerState(dataState: dataState, isLoading: true)); + + final String ip4 = dataState.serverDetails!.ip4; + final String domainName = dataState.serverDomain!.domainName; + + final Map matches = await repository.isDnsAddressesMatch( + domainName, + ip4, + dataState.dnsMatches ?? {}, + ); + + if (matches.values.every((final bool value) => value)) { + final ServerHostingDetails server = await repository.startServer( + dataState.serverDetails!, + ); + await repository.saveServerDetails(server); + await repository.saveIsServerStarted(true); + + final ServerInstallationNotFinished newState = dataState.copyWith( + isServerStarted: true, + isLoading: false, + serverDetails: server, + ); + emit(newState); + runDelayed( + resetServerIfServerIsOkay, + const Duration(seconds: 60), + newState, + ); + } else { + final ServerInstallationNotFinished newState = dataState.copyWith( + isLoading: false, + dnsMatches: matches, + ); + emit(newState); + runDelayed( + startServerIfDnsIsOkay, + const Duration(seconds: 30), + newState, + ); + } + } + + void resetServerIfServerIsOkay({ + final ServerInstallationNotFinished? state, + }) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; + + emit(TimerState(dataState: dataState, isLoading: true)); + + final bool isServerWorking = await repository.isHttpServerWorking(); + + if (isServerWorking) { + const Duration pauseDuration = Duration(seconds: 30); + emit( + TimerState( + dataState: dataState, + timerStart: DateTime.now(), + isLoading: false, + duration: pauseDuration, + ), + ); + timer = Timer(pauseDuration, () async { + final ServerHostingDetails hetznerServerDetails = + await repository.restart(); + await repository.saveIsServerResetedFirstTime(true); + await repository.saveServerDetails(hetznerServerDetails); + + final ServerInstallationNotFinished newState = dataState.copyWith( + isServerResetedFirstTime: true, + serverDetails: hetznerServerDetails, + isLoading: false, + ); + + emit(newState); + runDelayed(oneMoreReset, const Duration(seconds: 60), newState); + }); + } else { + runDelayed( + resetServerIfServerIsOkay, + const Duration(seconds: 60), + dataState, + ); + } + } + + void oneMoreReset({final ServerInstallationNotFinished? state}) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; + + emit(TimerState(dataState: dataState, isLoading: true)); + + final bool isServerWorking = await repository.isHttpServerWorking(); + + if (isServerWorking) { + const Duration pauseDuration = Duration(seconds: 30); + emit( + TimerState( + dataState: dataState, + timerStart: DateTime.now(), + isLoading: false, + duration: pauseDuration, + ), + ); + timer = Timer(pauseDuration, () async { + final ServerHostingDetails hetznerServerDetails = + await repository.restart(); + await repository.saveIsServerResetedSecondTime(true); + await repository.saveServerDetails(hetznerServerDetails); + + final ServerInstallationNotFinished newState = dataState.copyWith( + isServerResetedSecondTime: true, + serverDetails: hetznerServerDetails, + isLoading: false, + ); + + emit(newState); + runDelayed( + finishCheckIfServerIsOkay, + const Duration(seconds: 60), + newState, + ); + }); + } else { + runDelayed(oneMoreReset, const Duration(seconds: 60), dataState); + } + } + + void finishCheckIfServerIsOkay({ + final ServerInstallationNotFinished? state, + }) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; + + emit(TimerState(dataState: dataState, isLoading: true)); + + final bool isServerWorking = await repository.isHttpServerWorking(); + + if (isServerWorking) { + await repository.createDkimRecord(dataState.serverDomain!); + await repository.saveHasFinalChecked(true); + + emit(dataState.finish()); + } else { + runDelayed( + finishCheckIfServerIsOkay, + const Duration(seconds: 60), + dataState, + ); + } + } + + void runDelayed( + final void Function() work, + final Duration delay, + final ServerInstallationNotFinished? state, + ) async { + final ServerInstallationNotFinished dataState = + state ?? this.state as ServerInstallationNotFinished; + + emit( + TimerState( + dataState: dataState, + timerStart: DateTime.now(), + duration: delay, + isLoading: false, + ), + ); + timer = Timer(delay, work); + } + + void submitDomainForAccessRecovery(final String domain) async { + final ServerDomain serverDomain = ServerDomain( + domainName: domain, + provider: DnsProvider.unknown, + zoneId: '', + ); + final ServerRecoveryCapabilities recoveryCapabilities = + await repository.getRecoveryCapabilities(serverDomain); + + await repository.saveDomain(serverDomain); + await repository.saveIsRecoveringServer(true); + + emit( + ServerInstallationRecovery( + serverDomain: serverDomain, + recoveryCapabilities: recoveryCapabilities, + currentStep: RecoveryStep.selecting, + ), + ); + } + + void tryToRecover( + final String token, + final ServerRecoveryMethods method, + ) async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final ServerDomain? serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + try { + Future Function( + ServerDomain, + String, + ServerRecoveryCapabilities, + ) recoveryFunction; + switch (method) { + case ServerRecoveryMethods.newDeviceKey: + recoveryFunction = repository.authorizeByNewDeviceKey; + break; + case ServerRecoveryMethods.recoveryKey: + recoveryFunction = repository.authorizeByRecoveryKey; + break; + case ServerRecoveryMethods.oldToken: + recoveryFunction = repository.authorizeByApiToken; + break; + default: + throw Exception('Unknown recovery method'); + } + final ServerHostingDetails serverDetails = await recoveryFunction( + serverDomain, + token, + dataState.recoveryCapabilities, + ); + await repository.saveServerDetails(serverDetails); + emit( + dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.hetznerToken, + ), + ); + } on ServerAuthorizationException { + getIt() + .showSnackBar('recovering.authorization_failed'.tr()); + return; + } on IpNotFoundException { + getIt() + .showSnackBar('recovering.domain_recover_error'.tr()); + return; + } + } + + void revertRecoveryStep() { + if (state is ServerInstallationEmpty) { + return; + } + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + switch (dataState.currentStep) { + case RecoveryStep.selecting: + repository.deleteDomain(); + emit(const ServerInstallationEmpty()); + break; + case RecoveryStep.recoveryKey: + case RecoveryStep.newDeviceKey: + case RecoveryStep.oldToken: + emit( + dataState.copyWith( + currentStep: RecoveryStep.selecting, + ), + ); + break; + case RecoveryStep.serverSelection: + repository.deleteHetznerKey(); + emit( + dataState.copyWith( + currentStep: RecoveryStep.hetznerToken, + ), + ); + break; + // We won't revert steps after client is authorized + default: + break; + } + } + + void selectRecoveryMethod(final ServerRecoveryMethods method) { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + switch (method) { + case ServerRecoveryMethods.newDeviceKey: + emit( + dataState.copyWith( + currentStep: RecoveryStep.newDeviceKey, + ), + ); + break; + case ServerRecoveryMethods.recoveryKey: + emit( + dataState.copyWith( + currentStep: RecoveryStep.recoveryKey, + ), + ); + break; + case ServerRecoveryMethods.oldToken: + emit( + dataState.copyWith( + currentStep: RecoveryStep.oldToken, + ), + ); + break; + } + } + + Future> + getServersOnHetznerAccount() async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final List servers = + await repository.getServersOnHetznerAccount(); + final Iterable validated = servers.map( + (final ServerBasicInfo server) => + ServerBasicInfoWithValidators.fromServerBasicInfo( + serverBasicInfo: server, + isIpValid: server.ip == dataState.serverDetails?.ip4, + isReverseDnsValid: + server.reverseDns == dataState.serverDomain?.domainName, + ), + ); + return validated.toList(); + } + + Future setServerId(final ServerBasicInfo server) async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final ServerDomain? serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + final ServerHostingDetails serverDetails = ServerHostingDetails( + ip4: server.ip, + id: server.id, + createTime: server.created, + volume: ServerVolume( + id: server.volumeId, + name: 'recovered_volume', + ), + apiToken: dataState.serverDetails!.apiToken, + provider: ServerProvider.hetzner, + ); + await repository.saveDomain(serverDomain); + await repository.saveServerDetails(serverDetails); + emit( + dataState.copyWith( + serverDetails: serverDetails, + currentStep: RecoveryStep.cloudflareToken, + ), + ); + } + + Future setAndValidateCloudflareToken(final String token) async { + final ServerInstallationRecovery dataState = + state as ServerInstallationRecovery; + final ServerDomain? serverDomain = dataState.serverDomain; + if (serverDomain == null) { + return; + } + final String? zoneId = + await repository.getDomainId(token, serverDomain.domainName); + if (zoneId == null) { + getIt() + .showSnackBar('recovering.domain_not_available_on_token'.tr()); + return; + } + await repository.saveDomain( + ServerDomain( + domainName: serverDomain.domainName, + zoneId: zoneId, + provider: DnsProvider.cloudflare, + ), + ); + await repository.saveCloudFlareKey(token); + emit( + dataState.copyWith( + serverDomain: ServerDomain( + domainName: serverDomain.domainName, + zoneId: zoneId, + provider: DnsProvider.cloudflare, + ), + cloudFlareKey: token, + currentStep: RecoveryStep.backblazeToken, + ), + ); + } + + void finishRecoveryProcess( + final BackblazeCredential backblazeCredential, + ) async { + await repository.saveIsServerStarted(true); + await repository.saveIsServerResetedFirstTime(true); + await repository.saveIsServerResetedSecondTime(true); + await repository.saveHasFinalChecked(true); + await repository.saveIsRecoveringServer(false); + final User mainUser = await repository.getMainUser(); + await repository.saveRootUser(mainUser); + final ServerInstallationRecovery updatedState = + (state as ServerInstallationRecovery).copyWith( + backblazeCredential: backblazeCredential, + rootUser: mainUser, + ); + emit(updatedState.finish()); + } + + @override + void onChange(final Change change) { + super.onChange(change); + print('================================'); + print('ServerInstallationState changed!'); + print('Current type: ${change.nextState.runtimeType}'); + print('Hetzner key: ${change.nextState.hetznerKey}'); + print('Cloudflare key: ${change.nextState.cloudFlareKey}'); + print('Domain: ${change.nextState.serverDomain}'); + print('BackblazeCredential: ${change.nextState.backblazeCredential}'); + if (change.nextState is ServerInstallationRecovery) { + print( + 'Recovery Step: ${(change.nextState as ServerInstallationRecovery).currentStep}', + ); + print( + 'Recovery Capabilities: ${(change.nextState as ServerInstallationRecovery).recoveryCapabilities}', + ); + } + if (change.nextState is TimerState) { + print('Timer: ${(change.nextState as TimerState).duration}'); + } + } + + void clearAppConfig() { + closeTimer(); + + repository.clearAppConfig(); + emit(const ServerInstallationEmpty()); + } + + Future serverDelete() async { + closeTimer(); + + if (state.serverDetails != null) { + await repository.deleteServer(state.serverDomain!); + } + await repository.deleteServerRelatedRecords(); + emit( + ServerInstallationNotFinished( + hetznerKey: state.hetznerKey, + serverDomain: state.serverDomain, + cloudFlareKey: state.cloudFlareKey, + backblazeCredential: state.backblazeCredential, + rootUser: state.rootUser, + serverDetails: null, + isServerStarted: false, + isServerResetedFirstTime: false, + isServerResetedSecondTime: false, + isLoading: false, + dnsMatches: null, + ), + ); + } + + @override + Future close() { + closeTimer(); + return super.close(); + } + + void closeTimer() { + if (timer != null && timer!.isActive) { + timer!.cancel(); + } + } +} diff --git a/lib/logic/cubit/server_installation/server_installation_repository.dart b/lib/logic/cubit/server_installation/server_installation_repository.dart new file mode 100644 index 00000000..5d4db8fe --- /dev/null +++ b/lib/logic/cubit/server_installation/server_installation_repository.dart @@ -0,0 +1,683 @@ +import 'dart:io'; + +import 'package:basic_utils/basic_utils.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/foundation.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/cloudflare.dart'; +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.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/device_token.dart'; +import 'package:selfprivacy/logic/models/json/hetzner_server_info.dart'; +import 'package:selfprivacy/logic/models/message.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; +import 'package:selfprivacy/ui/components/action_button/action_button.dart'; +import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; + +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; + +class IpNotFoundException implements Exception { + IpNotFoundException(this.message); + final String message; +} + +class ServerAuthorizationException implements Exception { + ServerAuthorizationException(this.message); + final String message; +} + +class ServerInstallationRepository { + Box box = Hive.box(BNames.serverInstallationBox); + Box usersBox = Hive.box(BNames.usersBox); + + Future load() async { + final String? hetznerToken = getIt().hetznerKey; + final String? cloudflareToken = getIt().cloudFlareKey; + final ServerDomain? serverDomain = getIt().serverDomain; + final BackblazeCredential? backblazeCredential = + getIt().backblazeCredential; + final ServerHostingDetails? serverDetails = + getIt().serverDetails; + + if (box.get(BNames.hasFinalChecked, defaultValue: false)) { + return ServerInstallationFinished( + hetznerKey: hetznerToken!, + cloudFlareKey: cloudflareToken!, + serverDomain: serverDomain!, + backblazeCredential: backblazeCredential!, + serverDetails: serverDetails!, + rootUser: box.get(BNames.rootUser), + isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), + isServerResetedFirstTime: + box.get(BNames.isServerResetedFirstTime, defaultValue: false), + isServerResetedSecondTime: + box.get(BNames.isServerResetedSecondTime, defaultValue: false), + ); + } + + if (box.get(BNames.isRecoveringServer, defaultValue: false) && + serverDomain != null) { + return ServerInstallationRecovery( + hetznerKey: hetznerToken, + cloudFlareKey: cloudflareToken, + serverDomain: serverDomain, + backblazeCredential: backblazeCredential, + serverDetails: serverDetails, + rootUser: box.get(BNames.rootUser), + currentStep: _getCurrentRecoveryStep( + hetznerToken, + cloudflareToken, + serverDomain, + serverDetails, + ), + recoveryCapabilities: await getRecoveryCapabilities(serverDomain), + ); + } + + return ServerInstallationNotFinished( + hetznerKey: hetznerToken, + cloudFlareKey: cloudflareToken, + serverDomain: serverDomain, + backblazeCredential: backblazeCredential, + serverDetails: serverDetails, + rootUser: box.get(BNames.rootUser), + isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), + isServerResetedFirstTime: + box.get(BNames.isServerResetedFirstTime, defaultValue: false), + isServerResetedSecondTime: + box.get(BNames.isServerResetedSecondTime, defaultValue: false), + isLoading: box.get(BNames.isLoading, defaultValue: false), + dnsMatches: null, + ); + } + + RecoveryStep _getCurrentRecoveryStep( + final String? hetznerToken, + final String? cloudflareToken, + final ServerDomain serverDomain, + final ServerHostingDetails? serverDetails, + ) { + if (serverDetails != null) { + if (hetznerToken != null) { + if (serverDetails.provider != ServerProvider.unknown) { + if (serverDomain.provider != DnsProvider.unknown) { + return RecoveryStep.backblazeToken; + } + return RecoveryStep.cloudflareToken; + } + return RecoveryStep.serverSelection; + } + return RecoveryStep.hetznerToken; + } + return RecoveryStep.selecting; + } + + void clearAppConfig() { + box.clear(); + usersBox.clear(); + } + + Future startServer( + final ServerHostingDetails hetznerServer, + ) async { + final HetznerApi hetznerApi = HetznerApi(); + final ServerHostingDetails serverDetails = await hetznerApi.powerOn(); + + return serverDetails; + } + + Future getDomainId(final String token, final String domain) async { + final CloudflareApi cloudflareApi = CloudflareApi( + isWithToken: false, + customToken: token, + ); + + try { + final String domainId = await cloudflareApi.getZoneId(domain); + return domainId; + } on DomainNotFoundException { + return null; + } + } + + Future> isDnsAddressesMatch( + final String? domainName, + final String? ip4, + final Map skippedMatches, + ) async { + final List addresses = [ + '$domainName', + 'api.$domainName', + 'cloud.$domainName', + 'meet.$domainName', + 'password.$domainName' + ]; + + final Map matches = {}; + + for (final String address in addresses) { + if (skippedMatches[address] ?? false) { + matches[address] = true; + continue; + } + final List? lookupRecordRes = await DnsUtils.lookupRecord( + address, + RRecordType.A, + provider: DnsApiProvider.CLOUDFLARE, + ); + getIt.get().addMessage( + Message( + text: + 'DnsLookup: address: $address, $RRecordType, provider: CLOUDFLARE, ip4: $ip4', + ), + ); + getIt.get().addMessage( + Message( + text: + 'DnsLookup: ${lookupRecordRes == null ? 'empty' : (lookupRecordRes[0].data != ip4 ? 'wrong ip4' : 'right ip4')}', + ), + ); + if (lookupRecordRes == null || + lookupRecordRes.isEmpty || + lookupRecordRes[0].data != ip4) { + matches[address] = false; + } else { + matches[address] = true; + } + } + + return matches; + } + + Future createServer( + final User rootUser, + final String domainName, + final String cloudFlareKey, + final BackblazeCredential backblazeCredential, { + required final void Function() onCancel, + required final Future Function(ServerHostingDetails serverDetails) + onSuccess, + }) async { + final HetznerApi hetznerApi = HetznerApi(); + late ServerVolume dataBase; + + try { + dataBase = await hetznerApi.createVolume(); + + final ServerHostingDetails? serverDetails = await hetznerApi.createServer( + cloudFlareKey: cloudFlareKey, + rootUser: rootUser, + domainName: domainName, + dataBase: dataBase, + ); + if (serverDetails == null) { + print('Server is not initialized!'); + return; + } + saveServerDetails(serverDetails); + onSuccess(serverDetails); + } on DioError catch (e) { + if (e.response!.data['error']['code'] == 'uniqueness_error') { + final NavigationService nav = getIt.get(); + nav.showPopUpDialog( + BrandAlert( + title: 'modals.1'.tr(), + contentText: 'modals.2'.tr(), + actions: [ + ActionButton( + text: 'basis.delete'.tr(), + isRed: true, + onPressed: () async { + await hetznerApi.deleteSelfprivacyServerAndAllVolumes( + domainName: domainName, + ); + + final ServerHostingDetails? serverDetails = + await hetznerApi.createServer( + cloudFlareKey: cloudFlareKey, + rootUser: rootUser, + domainName: domainName, + dataBase: dataBase, + ); + if (serverDetails == null) { + print('Server is not initialized!'); + return; + } + await saveServerDetails(serverDetails); + onSuccess(serverDetails); + }, + ), + ActionButton( + text: 'basis.cancel'.tr(), + onPressed: onCancel, + ), + ], + ), + ); + } + } + } + + Future createDnsRecords( + final String ip4, + final ServerDomain cloudFlareDomain, { + required final void Function() onCancel, + }) async { + final CloudflareApi cloudflareApi = CloudflareApi(); + + await cloudflareApi.removeSimilarRecords( + ip4: ip4, + cloudFlareDomain: cloudFlareDomain, + ); + + try { + await cloudflareApi.createMultipleDnsRecords( + ip4: ip4, + cloudFlareDomain: cloudFlareDomain, + ); + } on DioError catch (e) { + final HetznerApi hetznerApi = HetznerApi(); + final NavigationService nav = getIt.get(); + nav.showPopUpDialog( + BrandAlert( + title: e.response!.data['errors'][0]['code'] == 1038 + ? 'modals.10'.tr() + : 'providers.domain.states.error'.tr(), + contentText: 'modals.6'.tr(), + actions: [ + ActionButton( + text: 'basis.delete'.tr(), + isRed: true, + onPressed: () async { + await hetznerApi.deleteSelfprivacyServerAndAllVolumes( + domainName: cloudFlareDomain.domainName, + ); + + onCancel(); + }, + ), + ActionButton( + text: 'basis.cancel'.tr(), + onPressed: onCancel, + ), + ], + ), + ); + } + + await HetznerApi().createReverseDns( + ip4: ip4, + domainName: cloudFlareDomain.domainName, + ); + } + + Future createDkimRecord(final ServerDomain cloudFlareDomain) async { + final CloudflareApi cloudflareApi = CloudflareApi(); + final ServerApi api = ServerApi(); + + final String? dkimRecordString = await api.getDkim(); + + await cloudflareApi.setDkim(dkimRecordString ?? '', cloudFlareDomain); + } + + Future isHttpServerWorking() async { + final ServerApi api = ServerApi(); + final bool isHttpServerWorking = await api.isHttpServerWorking(); + try { + await api.getDkim(); + } catch (e) { + return false; + } + return isHttpServerWorking; + } + + Future restart() async { + final HetznerApi hetznerApi = HetznerApi(); + return hetznerApi.reset(); + } + + Future powerOn() async { + final HetznerApi hetznerApi = HetznerApi(); + return hetznerApi.powerOn(); + } + + Future getRecoveryCapabilities( + final ServerDomain serverDomain, + ) async { + final ServerApi serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + ); + final String? serverApiVersion = await serverApi.getApiVersion(); + if (serverApiVersion == null) { + return ServerRecoveryCapabilities.none; + } + try { + final Version parsedVersion = Version.parse(serverApiVersion); + if (!VersionConstraint.parse('>=1.2.0').allows(parsedVersion)) { + return ServerRecoveryCapabilities.legacy; + } + return ServerRecoveryCapabilities.loginTokens; + } on FormatException { + return ServerRecoveryCapabilities.none; + } + } + + Future getServerIpFromDomain(final ServerDomain serverDomain) async { + final List? lookup = await DnsUtils.lookupRecord( + serverDomain.domainName, + RRecordType.A, + provider: DnsApiProvider.CLOUDFLARE, + ); + if (lookup == null || lookup.isEmpty) { + throw IpNotFoundException('No IP found for domain $serverDomain'); + } + return lookup[0].data; + } + + Future getDeviceName() async { + final DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (kIsWeb) { + return deviceInfo.webBrowserInfo.then( + (final WebBrowserInfo value) => + '${value.browserName} ${value.platform}', + ); + } else { + if (Platform.isAndroid) { + return deviceInfo.androidInfo.then( + (final AndroidDeviceInfo value) => + '${value.model} ${value.version.release}', + ); + } else if (Platform.isIOS) { + return deviceInfo.iosInfo.then( + (final IosDeviceInfo value) => + '${value.utsname.machine} ${value.systemName} ${value.systemVersion}', + ); + } else if (Platform.isLinux) { + return deviceInfo.linuxInfo + .then((final LinuxDeviceInfo value) => value.prettyName); + } else if (Platform.isMacOS) { + return deviceInfo.macOsInfo.then( + (final MacOsDeviceInfo value) => + '${value.hostName} ${value.computerName}', + ); + } else if (Platform.isWindows) { + return deviceInfo.windowsInfo + .then((final WindowsDeviceInfo value) => value.computerName); + } + } + return 'Unidentified'; + } + + Future authorizeByNewDeviceKey( + final ServerDomain serverDomain, + final String newDeviceKey, + final ServerRecoveryCapabilities recoveryCapabilities, + ) async { + final ServerApi serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + ); + final String serverIp = await getServerIpFromDomain(serverDomain); + final ApiResponse apiResponse = await serverApi.authorizeDevice( + DeviceToken(device: await getDeviceName(), token: newDeviceKey), + ); + + if (apiResponse.isSuccess) { + return ServerHostingDetails( + apiToken: apiResponse.data, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.unknown, + id: 0, + ip4: serverIp, + startTime: null, + createTime: null, + ); + } + + throw ServerAuthorizationException( + apiResponse.errorMessage ?? apiResponse.data, + ); + } + + Future authorizeByRecoveryKey( + final ServerDomain serverDomain, + final String recoveryKey, + final ServerRecoveryCapabilities recoveryCapabilities, + ) async { + final ServerApi serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + ); + final String serverIp = await getServerIpFromDomain(serverDomain); + final ApiResponse apiResponse = await serverApi.useRecoveryToken( + DeviceToken(device: await getDeviceName(), token: recoveryKey), + ); + + if (apiResponse.isSuccess) { + return ServerHostingDetails( + apiToken: apiResponse.data, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.unknown, + id: 0, + ip4: serverIp, + startTime: null, + createTime: null, + ); + } + + throw ServerAuthorizationException( + apiResponse.errorMessage ?? apiResponse.data, + ); + } + + Future authorizeByApiToken( + final ServerDomain serverDomain, + final String apiToken, + final ServerRecoveryCapabilities recoveryCapabilities, + ) async { + final ServerApi serverApi = ServerApi( + isWithToken: false, + overrideDomain: serverDomain.domainName, + customToken: apiToken, + ); + final String serverIp = await getServerIpFromDomain(serverDomain); + if (recoveryCapabilities == ServerRecoveryCapabilities.legacy) { + final Map apiResponse = + await serverApi.servicesPowerCheck(); + if (apiResponse.isNotEmpty) { + return ServerHostingDetails( + apiToken: apiToken, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.unknown, + id: 0, + ip4: serverIp, + startTime: null, + createTime: null, + ); + } else { + throw ServerAuthorizationException( + "Couldn't connect to server with this token", + ); + } + } + final ApiResponse deviceAuthKey = + await serverApi.createDeviceToken(); + final ApiResponse apiResponse = await serverApi.authorizeDevice( + DeviceToken(device: await getDeviceName(), token: deviceAuthKey.data), + ); + + if (apiResponse.isSuccess) { + return ServerHostingDetails( + apiToken: apiResponse.data, + volume: ServerVolume( + id: 0, + name: '', + ), + provider: ServerProvider.unknown, + id: 0, + ip4: serverIp, + startTime: null, + createTime: null, + ); + } + + throw ServerAuthorizationException( + apiResponse.errorMessage ?? apiResponse.data, + ); + } + + Future getMainUser() async { + final ServerApi serverApi = ServerApi(); + const User fallbackUser = User( + isFoundOnServer: false, + note: "Couldn't find main user on server, API is outdated", + login: 'UNKNOWN', + sshKeys: [], + ); + + final String? serverApiVersion = await serverApi.getApiVersion(); + final ApiResponse> users = + await serverApi.getUsersList(withMainUser: true); + if (serverApiVersion == null || !users.isSuccess) { + return fallbackUser; + } + try { + final Version parsedVersion = Version.parse(serverApiVersion); + if (!VersionConstraint.parse('>=1.2.5').allows(parsedVersion)) { + return fallbackUser; + } + return User( + isFoundOnServer: true, + login: users.data[0], + ); + } on FormatException { + return fallbackUser; + } + } + + Future> getServersOnHetznerAccount() async { + final HetznerApi hetznerApi = HetznerApi(); + final List servers = await hetznerApi.getServers(); + return servers + .map( + (final HetznerServerInfo server) => ServerBasicInfo( + id: server.id, + name: server.name, + ip: server.publicNet.ipv4.ip, + reverseDns: server.publicNet.ipv4.reverseDns, + created: server.created, + volumeId: server.volumes.isNotEmpty ? server.volumes[0] : 0, + ), + ) + .toList(); + } + + Future saveServerDetails( + final ServerHostingDetails serverDetails, + ) async { + await getIt().storeServerDetails(serverDetails); + } + + Future saveHetznerKey(final String key) async { + print('saved'); + await getIt().storeHetznerKey(key); + } + + Future deleteHetznerKey() async { + await box.delete(BNames.hetznerKey); + getIt().init(); + } + + Future saveBackblazeKey( + final BackblazeCredential backblazeCredential, + ) async { + await getIt().storeBackblazeCredential(backblazeCredential); + } + + Future saveCloudFlareKey(final String key) async { + await getIt().storeCloudFlareKey(key); + } + + Future saveDomain(final ServerDomain serverDomain) async { + await getIt().storeServerDomain(serverDomain); + } + + Future deleteDomain() async { + await box.delete(BNames.serverDomain); + getIt().init(); + } + + Future saveIsServerStarted(final bool value) async { + await box.put(BNames.isServerStarted, value); + } + + Future saveIsServerResetedFirstTime(final bool value) async { + await box.put(BNames.isServerResetedFirstTime, value); + } + + Future saveIsServerResetedSecondTime(final bool value) async { + await box.put(BNames.isServerResetedSecondTime, value); + } + + Future saveRootUser(final User rootUser) async { + await box.put(BNames.rootUser, rootUser); + } + + Future saveIsRecoveringServer(final bool value) async { + await box.put(BNames.isRecoveringServer, value); + } + + Future saveHasFinalChecked(final bool value) async { + await box.put(BNames.hasFinalChecked, value); + } + + Future deleteServer(final ServerDomain serverDomain) async { + final HetznerApi hetznerApi = HetznerApi(); + final CloudflareApi cloudFlare = CloudflareApi(); + + await hetznerApi.deleteSelfprivacyServerAndAllVolumes( + domainName: serverDomain.domainName, + ); + + await box.put(BNames.hasFinalChecked, false); + await box.put(BNames.isServerStarted, false); + await box.put(BNames.isServerResetedFirstTime, false); + await box.put(BNames.isServerResetedSecondTime, false); + await box.put(BNames.isLoading, false); + await box.put(BNames.serverDetails, null); + + await cloudFlare.removeSimilarRecords(cloudFlareDomain: serverDomain); + } + + Future deleteServerRelatedRecords() async { + await box.deleteAll([ + BNames.serverDetails, + BNames.isServerStarted, + BNames.isServerResetedFirstTime, + BNames.isServerResetedSecondTime, + BNames.hasFinalChecked, + BNames.isLoading, + ]); + getIt().init(); + } +} diff --git a/lib/logic/cubit/server_installation/server_installation_state.dart b/lib/logic/cubit/server_installation/server_installation_state.dart new file mode 100644 index 00000000..b3128e71 --- /dev/null +++ b/lib/logic/cubit/server_installation/server_installation_state.dart @@ -0,0 +1,322 @@ +part of '../server_installation/server_installation_cubit.dart'; + +abstract class ServerInstallationState extends Equatable { + const ServerInstallationState({ + required this.hetznerKey, + required this.cloudFlareKey, + required this.backblazeCredential, + required this.serverDomain, + required this.rootUser, + required this.serverDetails, + required this.isServerStarted, + required this.isServerResetedFirstTime, + required this.isServerResetedSecondTime, + }); + + @override + List get props => [ + hetznerKey, + cloudFlareKey, + backblazeCredential, + serverDomain, + rootUser, + serverDetails, + isServerStarted, + isServerResetedFirstTime, + ]; + + final String? hetznerKey; + final String? cloudFlareKey; + final BackblazeCredential? backblazeCredential; + final ServerDomain? serverDomain; + final User? rootUser; + final ServerHostingDetails? serverDetails; + final bool isServerStarted; + final bool isServerResetedFirstTime; + final bool isServerResetedSecondTime; + + bool get isHetznerFilled => hetznerKey != null; + bool get isCloudFlareFilled => cloudFlareKey != null; + bool get isBackblazeFilled => backblazeCredential != null; + bool get isDomainFilled => serverDomain != null; + bool get isUserFilled => rootUser != null; + bool get isServerCreated => serverDetails != null; + + bool get isFullyInitilized => _fulfilementList.every((final el) => el!); + ServerSetupProgress get progress => ServerSetupProgress + .values[_fulfilementList.where((final el) => el!).length]; + + int get porgressBar { + if (progress.index < 6) { + return progress.index; + } else if (progress.index < 10) { + return 6; + } else { + return 7; + } + } + + List get _fulfilementList { + final List res = [ + isHetznerFilled, + isCloudFlareFilled, + isBackblazeFilled, + isDomainFilled, + isUserFilled, + isServerCreated, + isServerStarted, + isServerResetedFirstTime, + isServerResetedSecondTime, + ]; + + return res; + } +} + +class TimerState extends ServerInstallationNotFinished { + TimerState({ + required this.dataState, + required final super.isLoading, + this.timerStart, + this.duration, + }) : super( + hetznerKey: dataState.hetznerKey, + cloudFlareKey: dataState.cloudFlareKey, + backblazeCredential: dataState.backblazeCredential, + serverDomain: dataState.serverDomain, + rootUser: dataState.rootUser, + serverDetails: dataState.serverDetails, + isServerStarted: dataState.isServerStarted, + isServerResetedFirstTime: dataState.isServerResetedFirstTime, + isServerResetedSecondTime: dataState.isServerResetedSecondTime, + dnsMatches: dataState.dnsMatches, + ); + + final ServerInstallationNotFinished dataState; + final DateTime? timerStart; + final Duration? duration; + + @override + List get props => [ + dataState, + timerStart, + duration, + ]; +} + +enum ServerSetupProgress { + nothingYet, + hetznerFilled, + cloudFlareFilled, + backblazeFilled, + domainFilled, + userFilled, + serverCreated, + serverStarted, + serverResetedFirstTime, + serverResetedSecondTime, +} + +class ServerInstallationNotFinished extends ServerInstallationState { + const ServerInstallationNotFinished({ + required final super.isServerStarted, + required final super.isServerResetedFirstTime, + required final super.isServerResetedSecondTime, + required final this.isLoading, + required this.dnsMatches, + final super.hetznerKey, + final super.cloudFlareKey, + final super.backblazeCredential, + final super.serverDomain, + final super.rootUser, + final super.serverDetails, + }); + final bool isLoading; + final Map? dnsMatches; + + @override + List get props => [ + hetznerKey, + cloudFlareKey, + backblazeCredential, + serverDomain, + rootUser, + serverDetails, + isServerStarted, + isServerResetedFirstTime, + isLoading, + dnsMatches, + ]; + + ServerInstallationNotFinished copyWith({ + final String? hetznerKey, + final String? cloudFlareKey, + final BackblazeCredential? backblazeCredential, + final ServerDomain? serverDomain, + final User? rootUser, + final ServerHostingDetails? serverDetails, + final bool? isServerStarted, + final bool? isServerResetedFirstTime, + final bool? isServerResetedSecondTime, + final bool? isLoading, + final Map? dnsMatches, + }) => + ServerInstallationNotFinished( + hetznerKey: hetznerKey ?? this.hetznerKey, + cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, + backblazeCredential: backblazeCredential ?? this.backblazeCredential, + serverDomain: serverDomain ?? this.serverDomain, + rootUser: rootUser ?? this.rootUser, + serverDetails: serverDetails ?? this.serverDetails, + isServerStarted: isServerStarted ?? this.isServerStarted, + isServerResetedFirstTime: + isServerResetedFirstTime ?? this.isServerResetedFirstTime, + isServerResetedSecondTime: + isServerResetedSecondTime ?? this.isServerResetedSecondTime, + isLoading: isLoading ?? this.isLoading, + dnsMatches: dnsMatches ?? this.dnsMatches, + ); + + ServerInstallationFinished finish() => ServerInstallationFinished( + hetznerKey: hetznerKey!, + cloudFlareKey: cloudFlareKey!, + backblazeCredential: backblazeCredential!, + serverDomain: serverDomain!, + rootUser: rootUser!, + serverDetails: serverDetails!, + isServerStarted: isServerStarted, + isServerResetedFirstTime: isServerResetedFirstTime, + isServerResetedSecondTime: isServerResetedSecondTime, + ); +} + +class ServerInstallationEmpty extends ServerInstallationNotFinished { + const ServerInstallationEmpty() + : super( + hetznerKey: null, + cloudFlareKey: null, + backblazeCredential: null, + serverDomain: null, + rootUser: null, + serverDetails: null, + isServerStarted: false, + isServerResetedFirstTime: false, + isServerResetedSecondTime: false, + isLoading: false, + dnsMatches: null, + ); +} + +class ServerInstallationFinished extends ServerInstallationState { + const ServerInstallationFinished({ + required final String super.hetznerKey, + required final String super.cloudFlareKey, + required final BackblazeCredential super.backblazeCredential, + required final ServerDomain super.serverDomain, + required final User super.rootUser, + required final ServerHostingDetails super.serverDetails, + required final super.isServerStarted, + required final super.isServerResetedFirstTime, + required final super.isServerResetedSecondTime, + }); + + @override + List get props => [ + hetznerKey, + cloudFlareKey, + backblazeCredential, + serverDomain, + rootUser, + serverDetails, + isServerStarted, + isServerResetedFirstTime, + ]; +} + +enum RecoveryStep { + selecting, + recoveryKey, + newDeviceKey, + oldToken, + hetznerToken, + serverSelection, + cloudflareToken, + backblazeToken, +} + +enum ServerRecoveryCapabilities { + none, + legacy, + loginTokens, +} + +enum ServerRecoveryMethods { + newDeviceKey, + recoveryKey, + oldToken, +} + +class ServerInstallationRecovery extends ServerInstallationState { + const ServerInstallationRecovery({ + required this.currentStep, + required this.recoveryCapabilities, + final super.hetznerKey, + final super.cloudFlareKey, + final super.backblazeCredential, + final super.serverDomain, + final super.rootUser, + final super.serverDetails, + }) : super( + isServerStarted: true, + isServerResetedFirstTime: true, + isServerResetedSecondTime: true, + ); + final RecoveryStep currentStep; + final ServerRecoveryCapabilities recoveryCapabilities; + + @override + List get props => [ + hetznerKey, + cloudFlareKey, + backblazeCredential, + serverDomain, + rootUser, + serverDetails, + isServerStarted, + isServerResetedFirstTime, + currentStep + ]; + + ServerInstallationRecovery copyWith({ + final String? hetznerKey, + final String? cloudFlareKey, + final BackblazeCredential? backblazeCredential, + final ServerDomain? serverDomain, + final User? rootUser, + final ServerHostingDetails? serverDetails, + final RecoveryStep? currentStep, + final ServerRecoveryCapabilities? recoveryCapabilities, + }) => + ServerInstallationRecovery( + hetznerKey: hetznerKey ?? this.hetznerKey, + cloudFlareKey: cloudFlareKey ?? this.cloudFlareKey, + backblazeCredential: backblazeCredential ?? this.backblazeCredential, + serverDomain: serverDomain ?? this.serverDomain, + rootUser: rootUser ?? this.rootUser, + serverDetails: serverDetails ?? this.serverDetails, + currentStep: currentStep ?? this.currentStep, + recoveryCapabilities: recoveryCapabilities ?? this.recoveryCapabilities, + ); + + ServerInstallationFinished finish() => ServerInstallationFinished( + hetznerKey: hetznerKey!, + cloudFlareKey: cloudFlareKey!, + backblazeCredential: backblazeCredential!, + serverDomain: serverDomain!, + rootUser: rootUser!, + serverDetails: serverDetails!, + isServerStarted: true, + isServerResetedFirstTime: true, + isServerResetedSecondTime: true, + ); +} diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart index 6d5dfef7..f83a2a9a 100644 --- a/lib/logic/cubit/services/services_cubit.dart +++ b/lib/logic/cubit/services/services_cubit.dart @@ -1,20 +1,17 @@ -import 'package:hive/hive.dart'; -import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/api_maps/server.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; part 'services_state.dart'; -class ServicesCubit extends AppConfigDependendCubit { - ServicesCubit(AppConfigCubit appConfigCubit) - : super(appConfigCubit, ServicesState.allOff()); - - Box box = Hive.box(BNames.servicesState); - final api = ServerApi(); +class ServicesCubit extends ServerInstallationDependendCubit { + ServicesCubit(final ServerInstallationCubit serverInstallationCubit) + : super(serverInstallationCubit, ServicesState.allOff()); + final ServerApi api = ServerApi(); + @override Future load() async { - if (appConfigCubit.state is AppConfigFinished) { - var statuses = await api.servicesPowerCheck(); + if (serverInstallationCubit.state is ServerInstallationFinished) { + final Map statuses = await api.servicesPowerCheck(); emit( ServicesState( isPasswordManagerEnable: statuses[ServiceTypes.passwordManager]!, @@ -29,7 +26,6 @@ class ServicesCubit extends AppConfigDependendCubit { @override void clear() async { - box.clear(); emit(ServicesState.allOff()); } } diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart index 3595e6b1..ffe90aee 100644 --- a/lib/logic/cubit/services/services_state.dart +++ b/lib/logic/cubit/services/services_state.dart @@ -1,6 +1,21 @@ part of 'services_cubit.dart'; -class ServicesState extends AppConfigDependendState { +class ServicesState extends ServerInstallationDependendState { + factory ServicesState.allOn() => const ServicesState( + isPasswordManagerEnable: true, + isCloudEnable: true, + isGitEnable: true, + isSocialNetworkEnable: true, + isVpnEnable: true, + ); + + factory ServicesState.allOff() => const ServicesState( + isPasswordManagerEnable: false, + isCloudEnable: false, + isGitEnable: false, + isSocialNetworkEnable: false, + isVpnEnable: false, + ); const ServicesState({ required this.isPasswordManagerEnable, required this.isCloudEnable, @@ -15,23 +30,8 @@ class ServicesState extends AppConfigDependendState { final bool isSocialNetworkEnable; final bool isVpnEnable; - factory ServicesState.allOff() => ServicesState( - isPasswordManagerEnable: false, - isCloudEnable: false, - isGitEnable: false, - isSocialNetworkEnable: false, - isVpnEnable: false, - ); - factory ServicesState.allOn() => ServicesState( - isPasswordManagerEnable: true, - isCloudEnable: true, - isGitEnable: true, - isSocialNetworkEnable: true, - isVpnEnable: true, - ); - ServicesState enableList( - List list, + final List list, ) => ServicesState( isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager) @@ -48,7 +48,7 @@ class ServicesState extends AppConfigDependendState { ); ServicesState disableList( - List list, + final List list, ) => ServicesState( isPasswordManagerEnable: list.contains(ServiceTypes.passwordManager) @@ -74,7 +74,7 @@ class ServicesState extends AppConfigDependendState { isVpnEnable ]; - bool isEnableByType(ServiceTypes type) { + bool isEnableByType(final ServiceTypes type) { switch (type) { case ServiceTypes.passwordManager: return isPasswordManagerEnable; diff --git a/lib/logic/cubit/users/users_cubit.dart b/lib/logic/cubit/users/users_cubit.dart index 0fd27064..9b86c109 100644 --- a/lib/logic/cubit/users/users_cubit.dart +++ b/lib/logic/cubit/users/users_cubit.dart @@ -1,131 +1,169 @@ -import 'package:bloc/bloc.dart'; import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; -import '../../api_maps/server.dart'; +import 'package:selfprivacy/logic/api_maps/server.dart'; export 'package:provider/provider.dart'; part 'users_state.dart'; -class UsersCubit extends AppConfigDependendCubit { - UsersCubit(AppConfigCubit appConfigCubit) +class UsersCubit extends ServerInstallationDependendCubit { + UsersCubit(final ServerInstallationCubit serverInstallationCubit) : super( - appConfigCubit, - UsersState( - [], User(login: 'root'), User(login: 'loading...'))); - Box box = Hive.box(BNames.users); - Box configBox = Hive.box(BNames.appConfig); + serverInstallationCubit, + const UsersState( + [], + User(login: 'root'), + User(login: 'loading...'), + ), + ); + Box box = Hive.box(BNames.usersBox); + Box serverInstallationBox = Hive.box(BNames.serverInstallationBox); - final api = ServerApi(); + final ServerApi api = ServerApi(); + @override Future load() async { - if (appConfigCubit.state is AppConfigFinished) { - var loadedUsers = box.values.toList(); - final primaryUser = configBox.get(BNames.rootUser, - defaultValue: User(login: 'loading...')); - List rootKeys = [ - ...configBox.get(BNames.rootKeys, defaultValue: []) + if (serverInstallationCubit.state is ServerInstallationFinished) { + final List loadedUsers = box.values.toList(); + final primaryUser = serverInstallationBox.get( + BNames.rootUser, + defaultValue: const User(login: 'loading...'), + ); + final List rootKeys = [ + ...serverInstallationBox.get(BNames.rootKeys, defaultValue: []) ]; if (loadedUsers.isNotEmpty) { - emit(UsersState( - loadedUsers, User(login: 'root', sshKeys: rootKeys), primaryUser)); + emit( + UsersState( + loadedUsers, + User(login: 'root', sshKeys: rootKeys), + primaryUser, + ), + ); } - final usersFromServer = await api.getUsersList(); + final ApiResponse> usersFromServer = + await api.getUsersList(); if (usersFromServer.isSuccess) { - final updatedList = + final List updatedList = mergeLocalAndServerUsers(loadedUsers, usersFromServer.data); - emit(UsersState( - updatedList, User(login: 'root', sshKeys: rootKeys), primaryUser)); + emit( + UsersState( + updatedList, + User(login: 'root', sshKeys: rootKeys), + primaryUser, + ), + ); } - final usersWithSshKeys = await loadSshKeys(state.users); + final List usersWithSshKeys = await loadSshKeys(state.users); // Update the users it the box box.clear(); box.addAll(usersWithSshKeys); - final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; - configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); - final primaryUserWithSshKeys = + final User rootUserWithSshKeys = + (await loadSshKeys([state.rootUser])).first; + serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + final User primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; - configBox.put(BNames.rootUser, primaryUserWithSshKeys); - emit(UsersState( - usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); + emit( + UsersState( + usersWithSshKeys, + rootUserWithSshKeys, + primaryUserWithSshKeys, + ), + ); } } List mergeLocalAndServerUsers( - List localUsers, List serverUsers) { + final List localUsers, + final List serverUsers, + ) { // If local user not exists on server, add it with isFoundOnServer = false // If server user not exists on local, add it - List mergedUsers = []; - List serverUsersCopy = List.from(serverUsers); + final List mergedUsers = []; + final List serverUsersCopy = List.from(serverUsers); - for (var localUser in localUsers) { + for (final User localUser in localUsers) { if (serverUsersCopy.contains(localUser.login)) { - mergedUsers.add(User( - login: localUser.login, - isFoundOnServer: true, - password: localUser.password, - sshKeys: localUser.sshKeys, - )); + mergedUsers.add( + User( + login: localUser.login, + isFoundOnServer: true, + password: localUser.password, + sshKeys: localUser.sshKeys, + ), + ); serverUsersCopy.remove(localUser.login); } else { - mergedUsers.add(User( - login: localUser.login, - isFoundOnServer: false, - password: localUser.password, - note: localUser.note, - )); + mergedUsers.add( + User( + login: localUser.login, + isFoundOnServer: false, + password: localUser.password, + note: localUser.note, + ), + ); } } - for (var serverUser in serverUsersCopy) { - mergedUsers.add(User( - login: serverUser, - isFoundOnServer: true, - )); + for (final String serverUser in serverUsersCopy) { + mergedUsers.add( + User( + login: serverUser, + isFoundOnServer: true, + ), + ); } return mergedUsers; } - Future> loadSshKeys(List users) async { - List updatedUsers = []; + Future> loadSshKeys(final List users) async { + final List updatedUsers = []; - for (var user in users) { + for (final User user in users) { if (user.isFoundOnServer || user.login == 'root' || user.login == state.primaryUser.login) { - final sshKeys = await api.getUserSshKeys(user); + final ApiResponse> sshKeys = + await api.getUserSshKeys(user); print('sshKeys for $user: ${sshKeys.data}'); if (sshKeys.isSuccess) { - updatedUsers.add(User( - login: user.login, - isFoundOnServer: true, - password: user.password, - sshKeys: sshKeys.data, - note: user.note, - )); + updatedUsers.add( + User( + login: user.login, + isFoundOnServer: true, + password: user.password, + sshKeys: sshKeys.data, + note: user.note, + ), + ); } else { - updatedUsers.add(User( - login: user.login, - isFoundOnServer: true, - password: user.password, - note: user.note, - )); + updatedUsers.add( + User( + login: user.login, + isFoundOnServer: true, + password: user.password, + note: user.note, + ), + ); } } else { - updatedUsers.add(User( - login: user.login, - isFoundOnServer: false, - password: user.password, - note: user.note, - )); + updatedUsers.add( + User( + login: user.login, + isFoundOnServer: false, + password: user.password, + note: user.note, + ), + ); } } return updatedUsers; @@ -133,27 +171,34 @@ class UsersCubit extends AppConfigDependendCubit { Future refresh() async { List updatedUsers = List.from(state.users); - final usersFromServer = await api.getUsersList(); + final ApiResponse> usersFromServer = await api.getUsersList(); if (usersFromServer.isSuccess) { updatedUsers = mergeLocalAndServerUsers(updatedUsers, usersFromServer.data); } - final usersWithSshKeys = await loadSshKeys(updatedUsers); + final List usersWithSshKeys = await loadSshKeys(updatedUsers); box.clear(); box.addAll(usersWithSshKeys); - final rootUserWithSshKeys = (await loadSshKeys([state.rootUser])).first; - configBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); - final primaryUserWithSshKeys = + final User rootUserWithSshKeys = + (await loadSshKeys([state.rootUser])).first; + serverInstallationBox.put(BNames.rootKeys, rootUserWithSshKeys.sshKeys); + final User primaryUserWithSshKeys = (await loadSshKeys([state.primaryUser])).first; - configBox.put(BNames.rootUser, primaryUserWithSshKeys); - emit(UsersState( - usersWithSshKeys, rootUserWithSshKeys, primaryUserWithSshKeys)); + serverInstallationBox.put(BNames.rootUser, primaryUserWithSshKeys); + emit( + UsersState( + usersWithSshKeys, + rootUserWithSshKeys, + primaryUserWithSshKeys, + ), + ); return; } - Future createUser(User user) async { + Future createUser(final User user) async { // If user exists on server, do nothing - if (state.users.any((u) => u.login == user.login && u.isFoundOnServer)) { + if (state.users + .any((final User u) => u.login == user.login && u.isFoundOnServer)) { return; } // If user is root or primary user, do nothing @@ -161,78 +206,82 @@ class UsersCubit extends AppConfigDependendCubit { return; } // If API returned error, do nothing - final result = await api.createUser(user); + final ApiResponse result = await api.createUser(user); if (!result.isSuccess) { return; } - var loadedUsers = List.from(state.users); + final List loadedUsers = List.from(state.users); loadedUsers.add(result.data); await box.clear(); await box.addAll(loadedUsers); emit(state.copyWith(users: loadedUsers)); } - Future deleteUser(User user) async { + Future deleteUser(final User user) async { // If user is primary or root, don't delete if (user.login == state.primaryUser.login || user.login == 'root') { return; } - var loadedUsers = List.from(state.users); - final result = await api.deleteUser(user); + final List loadedUsers = List.from(state.users); + final bool result = await api.deleteUser(user); if (result) { - loadedUsers.removeWhere((u) => u.login == user.login); + loadedUsers.removeWhere((final User u) => u.login == user.login); await box.clear(); await box.addAll(loadedUsers); emit(state.copyWith(users: loadedUsers)); } } - Future addSshKey(User user, String publicKey) async { + Future addSshKey(final User user, final String publicKey) async { // If adding root key, use api.addRootSshKey // Otherwise, use api.addUserSshKey if (user.login == 'root') { - final result = await api.addRootSshKey(publicKey); + final ApiResponse result = await api.addRootSshKey(publicKey); if (result.isSuccess) { // Add ssh key to the array of root keys - final rootKeys = - configBox.get(BNames.rootKeys, defaultValue: []) as List; + final List rootKeys = serverInstallationBox + .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.add(publicKey); - configBox.put(BNames.rootKeys, rootKeys); - emit(state.copyWith( - rootUser: User( - login: state.rootUser.login, - isFoundOnServer: true, - password: state.rootUser.password, - sshKeys: rootKeys, - note: state.rootUser.note, + serverInstallationBox.put(BNames.rootKeys, rootKeys); + emit( + state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), ), - )); + ); } } else { - final result = await api.addUserSshKey(user, publicKey); + final ApiResponse result = await api.addUserSshKey(user, publicKey); if (result.isSuccess) { // If it is primary user, update primary user if (user.login == state.primaryUser.login) { - List primaryUserKeys = + final List primaryUserKeys = List.from(state.primaryUser.sshKeys); primaryUserKeys.add(publicKey); - final updatedUser = User( + final User updatedUser = User( login: state.primaryUser.login, isFoundOnServer: true, password: state.primaryUser.password, sshKeys: primaryUserKeys, note: state.primaryUser.note, ); - configBox.put(BNames.rootUser, updatedUser); - emit(state.copyWith( - primaryUser: updatedUser, - )); + serverInstallationBox.put(BNames.rootUser, updatedUser); + emit( + state.copyWith( + primaryUser: updatedUser, + ), + ); } else { // If it is not primary user, update user - List userKeys = List.from(user.sshKeys); + final List userKeys = List.from(user.sshKeys); userKeys.add(publicKey); - final updatedUser = User( + final User updatedUser = User( login: user.login, isFoundOnServer: true, password: user.password, @@ -240,59 +289,66 @@ class UsersCubit extends AppConfigDependendCubit { note: user.note, ); await box.putAt(box.values.toList().indexOf(user), updatedUser); - emit(state.copyWith( - users: box.values.toList(), - )); + emit( + state.copyWith( + users: box.values.toList(), + ), + ); } } } } - Future deleteSshKey(User user, String publicKey) async { + Future deleteSshKey(final User user, final String publicKey) async { // All keys are deleted via api.deleteUserSshKey - final result = await api.deleteUserSshKey(user, publicKey); + final ApiResponse result = + await api.deleteUserSshKey(user, publicKey); if (result.isSuccess) { // If it is root user, delete key from root keys // If it is primary user, update primary user // If it is not primary user, update user if (user.login == 'root') { - final rootKeys = - configBox.get(BNames.rootKeys, defaultValue: []) as List; + final List rootKeys = serverInstallationBox + .get(BNames.rootKeys, defaultValue: []) as List; rootKeys.remove(publicKey); - configBox.put(BNames.rootKeys, rootKeys); - emit(state.copyWith( - rootUser: User( - login: state.rootUser.login, - isFoundOnServer: true, - password: state.rootUser.password, - sshKeys: rootKeys, - note: state.rootUser.note, + serverInstallationBox.put(BNames.rootKeys, rootKeys); + emit( + state.copyWith( + rootUser: User( + login: state.rootUser.login, + isFoundOnServer: true, + password: state.rootUser.password, + sshKeys: rootKeys, + note: state.rootUser.note, + ), ), - )); + ); return; } if (user.login == state.primaryUser.login) { - List primaryUserKeys = + final List primaryUserKeys = List.from(state.primaryUser.sshKeys); primaryUserKeys.remove(publicKey); - final updatedUser = User( + final User updatedUser = User( login: state.primaryUser.login, isFoundOnServer: true, password: state.primaryUser.password, sshKeys: primaryUserKeys, note: state.primaryUser.note, ); - configBox.put(BNames.rootUser, updatedUser); - emit(state.copyWith( - primaryUser: updatedUser, - )); + serverInstallationBox.put(BNames.rootUser, updatedUser); + emit( + state.copyWith( + primaryUser: updatedUser, + ), + ); return; } - List userKeys = List.from(user.sshKeys); + final List userKeys = List.from(user.sshKeys); userKeys.remove(publicKey); - final updatedUser = User( + final User updatedUser = User( login: user.login, isFoundOnServer: true, password: user.password, @@ -300,14 +356,22 @@ class UsersCubit extends AppConfigDependendCubit { note: user.note, ); await box.putAt(box.values.toList().indexOf(user), updatedUser); - emit(state.copyWith( - users: box.values.toList(), - )); + emit( + state.copyWith( + users: box.values.toList(), + ), + ); } } @override void clear() async { - emit(UsersState([], User(login: 'root'), User(login: 'loading...'))); + emit( + const UsersState( + [], + User(login: 'root'), + User(login: 'loading...'), + ), + ); } } diff --git a/lib/logic/cubit/users/users_state.dart b/lib/logic/cubit/users/users_state.dart index d15789c9..fa4ed1cd 100644 --- a/lib/logic/cubit/users/users_state.dart +++ b/lib/logic/cubit/users/users_state.dart @@ -1,6 +1,6 @@ part of 'users_cubit.dart'; -class UsersState extends AppConfigDependendState { +class UsersState extends ServerInstallationDependendState { const UsersState(this.users, this.rootUser, this.primaryUser); final List users; @@ -11,22 +11,20 @@ class UsersState extends AppConfigDependendState { List get props => [users, rootUser, primaryUser]; UsersState copyWith({ - List? users, - User? rootUser, - User? primaryUser, - }) { - return UsersState( - users ?? this.users, - rootUser ?? this.rootUser, - primaryUser ?? this.primaryUser, - ); - } + final List? users, + final User? rootUser, + final User? primaryUser, + }) => + UsersState( + users ?? this.users, + rootUser ?? this.rootUser, + primaryUser ?? this.primaryUser, + ); - bool isLoginRegistered(String login) { - return users.any((user) => user.login == login) || - login == rootUser.login || - login == primaryUser.login; - } + bool isLoginRegistered(final String login) => + users.any((final User user) => user.login == login) || + login == rootUser.login || + login == primaryUser.login; bool get isEmpty => users.isEmpty; } diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart index 1bc15eb1..3f3e5ac0 100644 --- a/lib/logic/get_it/api_config.dart +++ b/lib/logic/get_it/api_config.dart @@ -1,64 +1,64 @@ import 'package:hive/hive.dart'; import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/logic/models/backblaze_bucket.dart'; -import 'package:selfprivacy/logic/models/backblaze_credential.dart'; -import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; -import 'package:selfprivacy/logic/models/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_bucket.dart'; +import 'package:selfprivacy/logic/models/hive/backblaze_credential.dart'; +import 'package:selfprivacy/logic/models/hive/server_details.dart'; +import 'package:selfprivacy/logic/models/hive/server_domain.dart'; class ApiConfigModel { - Box _box = Hive.box(BNames.appConfig); + final Box _box = Hive.box(BNames.serverInstallationBox); - HetznerServerDetails? get hetznerServer => _hetznerServer; + ServerHostingDetails? get serverDetails => _serverDetails; String? get hetznerKey => _hetznerKey; String? get cloudFlareKey => _cloudFlareKey; BackblazeCredential? get backblazeCredential => _backblazeCredential; - CloudFlareDomain? get cloudFlareDomain => _cloudFlareDomain; + ServerDomain? get serverDomain => _serverDomain; BackblazeBucket? get backblazeBucket => _backblazeBucket; String? _hetznerKey; String? _cloudFlareKey; - HetznerServerDetails? _hetznerServer; + ServerHostingDetails? _serverDetails; BackblazeCredential? _backblazeCredential; - CloudFlareDomain? _cloudFlareDomain; + ServerDomain? _serverDomain; BackblazeBucket? _backblazeBucket; - Future storeHetznerKey(String value) async { + Future storeHetznerKey(final String value) async { await _box.put(BNames.hetznerKey, value); _hetznerKey = value; } - Future storeCloudFlareKey(String value) async { + Future storeCloudFlareKey(final String value) async { await _box.put(BNames.cloudFlareKey, value); _cloudFlareKey = value; } - Future storeBackblazeCredential(BackblazeCredential value) async { - await _box.put(BNames.backblazeKey, value); + Future storeBackblazeCredential(final BackblazeCredential value) async { + await _box.put(BNames.backblazeCredential, value); _backblazeCredential = value; } - Future storeCloudFlareDomain(CloudFlareDomain value) async { - await _box.put(BNames.cloudFlareDomain, value); - _cloudFlareDomain = value; + Future storeServerDomain(final ServerDomain value) async { + await _box.put(BNames.serverDomain, value); + _serverDomain = value; } - Future storeServerDetails(HetznerServerDetails value) async { - await _box.put(BNames.hetznerServer, value); - _hetznerServer = value; + Future storeServerDetails(final ServerHostingDetails value) async { + await _box.put(BNames.serverDetails, value); + _serverDetails = value; } - Future storeBackblazeBucket(BackblazeBucket value) async { + Future storeBackblazeBucket(final BackblazeBucket value) async { await _box.put(BNames.backblazeBucket, value); _backblazeBucket = value; } - clear() { + void clear() { _hetznerKey = null; _cloudFlareKey = null; _backblazeCredential = null; - _cloudFlareDomain = null; - _hetznerServer = null; + _serverDomain = null; + _serverDetails = null; _backblazeBucket = null; } @@ -66,9 +66,9 @@ class ApiConfigModel { _hetznerKey = _box.get(BNames.hetznerKey); _cloudFlareKey = _box.get(BNames.cloudFlareKey); - _backblazeCredential = _box.get(BNames.backblazeKey); - _cloudFlareDomain = _box.get(BNames.cloudFlareDomain); - _hetznerServer = _box.get(BNames.hetznerServer); + _backblazeCredential = _box.get(BNames.backblazeCredential); + _serverDomain = _box.get(BNames.serverDomain); + _serverDetails = _box.get(BNames.serverDetails); _backblazeBucket = _box.get(BNames.backblazeBucket); } } diff --git a/lib/logic/get_it/console.dart b/lib/logic/get_it/console.dart index 80dc5a37..290f31ab 100644 --- a/lib/logic/get_it/console.dart +++ b/lib/logic/get_it/console.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/models/message.dart'; class ConsoleModel extends ChangeNotifier { - List _messages = []; + final List _messages = []; List get messages => _messages; - void addMessage(Message message) { + void addMessage(final Message message) { messages.add(message); notifyListeners(); } diff --git a/lib/logic/get_it/navigation.dart b/lib/logic/get_it/navigation.dart index 0a235434..15adc982 100644 --- a/lib/logic/get_it/navigation.dart +++ b/lib/logic/get_it/navigation.dart @@ -9,18 +9,18 @@ class NavigationService { NavigatorState? get navigator => navigatorKey.currentState; - void showPopUpDialog(AlertDialog dialog) { - final context = navigatorKey.currentState!.overlay!.context; + void showPopUpDialog(final AlertDialog dialog) { + final BuildContext context = navigatorKey.currentState!.overlay!.context; showDialog( context: context, - builder: (_) => dialog, + builder: (final _) => dialog, ); } - void showSnackBar(String text) { - final state = scaffoldMessengerKey.currentState!; - final snack = SnackBar( + void showSnackBar(final String text) { + final ScaffoldMessengerState state = scaffoldMessengerKey.currentState!; + final SnackBar snack = SnackBar( backgroundColor: BrandColors.black.withOpacity(0.8), content: Text(text, style: buttonTitleText), duration: const Duration(seconds: 2), diff --git a/lib/logic/get_it/ssh.dart b/lib/logic/get_it/ssh.dart deleted file mode 100644 index 0e833c7e..00000000 --- a/lib/logic/get_it/ssh.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:hive/hive.dart'; -import 'package:pointycastle/pointycastle.dart'; -import 'package:rsa_encrypt/rsa_encrypt.dart'; -import 'package:selfprivacy/config/hive_config.dart'; -import 'package:pointycastle/api.dart' as crypto; -import 'package:ssh_key/ssh_key.dart' as ssh_key; - -class SSHModel { - Box _box = Hive.box(BNames.sshConfig); - String? savedPrivateKey; - String? savedPubKey; - - Future generateKeys() async { - var helper = RsaKeyHelper(); - crypto.AsymmetricKeyPair pair = - await helper.computeRSAKeyPair(helper.getSecureRandom()); - var privateKey = pair.privateKey as RSAPrivateKey; - var publicKey = pair.publicKey as RSAPublicKey; - - savedPrivateKey = helper.encodePrivateKeyToPemPKCS1(privateKey); - savedPubKey = publicKey.encode(ssh_key.PubKeyEncoding.openSsh); - - await _box.put(BNames.sshPrivateKey, savedPrivateKey); - await _box.put(BNames.sshPublicKey, savedPubKey); - } - - void init() async { - savedPrivateKey = _box.get(BNames.sshPrivateKey); - savedPubKey = _box.get(BNames.sshPublicKey); - } - - bool get isSSHKeyGenerated => savedPrivateKey != null && savedPubKey != null; - - Future clear() async { - savedPrivateKey = null; - savedPubKey = null; - await _box.clear(); - } -} diff --git a/lib/logic/models/cloudflare_domain.dart b/lib/logic/models/cloudflare_domain.dart deleted file mode 100644 index 9d85bfb1..00000000 --- a/lib/logic/models/cloudflare_domain.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:hive/hive.dart'; - -part 'cloudflare_domain.g.dart'; - -@HiveType(typeId: 3) -class CloudFlareDomain { - CloudFlareDomain({ - required this.domainName, - required this.zoneId, - }); - - @HiveField(0) - final String domainName; - - @HiveField(1) - final String zoneId; - - @override - String toString() { - return '$domainName: $zoneId'; - } -} diff --git a/lib/logic/models/cloudflare_domain.g.dart b/lib/logic/models/cloudflare_domain.g.dart deleted file mode 100644 index dcd95317..00000000 --- a/lib/logic/models/cloudflare_domain.g.dart +++ /dev/null @@ -1,44 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'cloudflare_domain.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class CloudFlareDomainAdapter extends TypeAdapter { - @override - final int typeId = 3; - - @override - CloudFlareDomain read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return CloudFlareDomain( - domainName: fields[0] as String, - zoneId: fields[1] as String, - ); - } - - @override - void write(BinaryWriter writer, CloudFlareDomain obj) { - writer - ..writeByte(2) - ..writeByte(0) - ..write(obj.domainName) - ..writeByte(1) - ..write(obj.zoneId); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is CloudFlareDomainAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/logic/models/hive/README.md b/lib/logic/models/hive/README.md new file mode 100644 index 00000000..afdd6276 --- /dev/null +++ b/lib/logic/models/hive/README.md @@ -0,0 +1,13 @@ +# Registered Hive Types + +1. User +2. ServerHostingDetails +3. ServerDomain +4. BackblazeCredential +5. ServerVolume +6. BackblazeBucket + + +## Enums +100. DnsProvider +101. ServerProvider \ No newline at end of file diff --git a/lib/logic/models/backblaze_bucket.dart b/lib/logic/models/hive/backblaze_bucket.dart similarity index 59% rename from lib/logic/models/backblaze_bucket.dart rename to lib/logic/models/hive/backblaze_bucket.dart index e1e0dd3f..39b98cf5 100644 --- a/lib/logic/models/backblaze_bucket.dart +++ b/lib/logic/models/hive/backblaze_bucket.dart @@ -4,11 +4,12 @@ part 'backblaze_bucket.g.dart'; @HiveType(typeId: 6) class BackblazeBucket { - BackblazeBucket( - {required this.bucketId, - required this.bucketName, - required this.applicationKeyId, - required this.applicationKey}); + BackblazeBucket({ + required this.bucketId, + required this.bucketName, + required this.applicationKeyId, + required this.applicationKey, + }); @HiveField(0) final String bucketId; @@ -23,7 +24,5 @@ class BackblazeBucket { final String bucketName; @override - String toString() { - return '$bucketName'; - } + String toString() => bucketName; } diff --git a/lib/logic/models/backblaze_bucket.g.dart b/lib/logic/models/hive/backblaze_bucket.g.dart similarity index 100% rename from lib/logic/models/backblaze_bucket.g.dart rename to lib/logic/models/hive/backblaze_bucket.g.dart diff --git a/lib/logic/models/backblaze_credential.dart b/lib/logic/models/hive/backblaze_credential.dart similarity index 52% rename from lib/logic/models/backblaze_credential.dart rename to lib/logic/models/hive/backblaze_credential.dart index b9c06364..d7bf2d06 100644 --- a/lib/logic/models/backblaze_credential.dart +++ b/lib/logic/models/hive/backblaze_credential.dart @@ -14,16 +14,14 @@ class BackblazeCredential { @HiveField(1) final String applicationKey; - get encodedApiKey => encodedBackblazeKey(keyId, applicationKey); + String get encodedApiKey => encodedBackblazeKey(keyId, applicationKey); @override - String toString() { - return '$keyId: $encodedApiKey'; - } + String toString() => '$keyId: $encodedApiKey'; } -String encodedBackblazeKey(String? keyId, String? applicationKey) { - String _apiKey = '$keyId:$applicationKey'; - String encodedApiKey = base64.encode(utf8.encode(_apiKey)); +String encodedBackblazeKey(final String? keyId, final String? applicationKey) { + final String apiKey = '$keyId:$applicationKey'; + final String encodedApiKey = base64.encode(utf8.encode(apiKey)); return encodedApiKey; } diff --git a/lib/logic/models/backblaze_credential.g.dart b/lib/logic/models/hive/backblaze_credential.g.dart similarity index 100% rename from lib/logic/models/backblaze_credential.g.dart rename to lib/logic/models/hive/backblaze_credential.g.dart diff --git a/lib/logic/models/hive/server_details.dart b/lib/logic/models/hive/server_details.dart new file mode 100644 index 00000000..5188e62e --- /dev/null +++ b/lib/logic/models/hive/server_details.dart @@ -0,0 +1,72 @@ +import 'package:hive/hive.dart'; + +part 'server_details.g.dart'; + +@HiveType(typeId: 2) +class ServerHostingDetails { + ServerHostingDetails({ + required this.ip4, + required this.id, + required this.createTime, + required this.volume, + required this.apiToken, + required this.provider, + this.startTime, + }); + + @HiveField(0) + final String ip4; + + @HiveField(1) + final int id; + + @HiveField(3) + final DateTime? createTime; + + @HiveField(2) + final DateTime? startTime; + + @HiveField(4) + final ServerVolume volume; + + @HiveField(5) + final String apiToken; + + @HiveField(6, defaultValue: ServerProvider.hetzner) + final ServerProvider provider; + + ServerHostingDetails copyWith({final DateTime? startTime}) => + ServerHostingDetails( + startTime: startTime ?? this.startTime, + createTime: createTime, + id: id, + ip4: ip4, + volume: volume, + apiToken: apiToken, + provider: provider, + ); + + @override + String toString() => id.toString(); +} + +@HiveType(typeId: 5) +class ServerVolume { + ServerVolume({ + required this.id, + required this.name, + }); + + @HiveField(1) + int id; + @HiveField(2) + String name; +} + +@HiveType(typeId: 101) +enum ServerProvider { + @HiveField(0) + unknown, + @HiveField(1) + hetzner, +} diff --git a/lib/logic/models/server_details.g.dart b/lib/logic/models/hive/server_details.g.dart similarity index 52% rename from lib/logic/models/server_details.g.dart rename to lib/logic/models/hive/server_details.g.dart index cba8848c..f10628e7 100644 --- a/lib/logic/models/server_details.g.dart +++ b/lib/logic/models/hive/server_details.g.dart @@ -6,30 +6,33 @@ part of 'server_details.dart'; // TypeAdapterGenerator // ************************************************************************** -class HetznerServerDetailsAdapter extends TypeAdapter { +class ServerHostingDetailsAdapter extends TypeAdapter { @override final int typeId = 2; @override - HetznerServerDetails read(BinaryReader reader) { + ServerHostingDetails read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return HetznerServerDetails( + return ServerHostingDetails( ip4: fields[0] as String, id: fields[1] as int, createTime: fields[3] as DateTime?, - dataBase: fields[4] as HetznerDataBase, + volume: fields[4] as ServerVolume, apiToken: fields[5] as String, + provider: fields[6] == null + ? ServerProvider.hetzner + : fields[6] as ServerProvider, startTime: fields[2] as DateTime?, ); } @override - void write(BinaryWriter writer, HetznerServerDetails obj) { + void write(BinaryWriter writer, ServerHostingDetails obj) { writer - ..writeByte(6) + ..writeByte(7) ..writeByte(0) ..write(obj.ip4) ..writeByte(1) @@ -39,9 +42,11 @@ class HetznerServerDetailsAdapter extends TypeAdapter { ..writeByte(2) ..write(obj.startTime) ..writeByte(4) - ..write(obj.dataBase) + ..write(obj.volume) ..writeByte(5) - ..write(obj.apiToken); + ..write(obj.apiToken) + ..writeByte(6) + ..write(obj.provider); } @override @@ -50,29 +55,29 @@ class HetznerServerDetailsAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is HetznerServerDetailsAdapter && + other is ServerHostingDetailsAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } -class HetznerDataBaseAdapter extends TypeAdapter { +class ServerVolumeAdapter extends TypeAdapter { @override final int typeId = 5; @override - HetznerDataBase read(BinaryReader reader) { + ServerVolume read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return HetznerDataBase( + return ServerVolume( id: fields[1] as int, name: fields[2] as String, ); } @override - void write(BinaryWriter writer, HetznerDataBase obj) { + void write(BinaryWriter writer, ServerVolume obj) { writer ..writeByte(2) ..writeByte(1) @@ -87,7 +92,46 @@ class HetznerDataBaseAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is HetznerDataBaseAdapter && + other is ServerVolumeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ServerProviderAdapter extends TypeAdapter { + @override + final int typeId = 101; + + @override + ServerProvider read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ServerProvider.unknown; + case 1: + return ServerProvider.hetzner; + default: + return ServerProvider.unknown; + } + } + + @override + void write(BinaryWriter writer, ServerProvider obj) { + switch (obj) { + case ServerProvider.unknown: + writer.writeByte(0); + break; + case ServerProvider.hetzner: + writer.writeByte(1); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ServerProviderAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/logic/models/hive/server_domain.dart b/lib/logic/models/hive/server_domain.dart new file mode 100644 index 00000000..9b5d32c1 --- /dev/null +++ b/lib/logic/models/hive/server_domain.dart @@ -0,0 +1,32 @@ +import 'package:hive/hive.dart'; + +part 'server_domain.g.dart'; + +@HiveType(typeId: 3) +class ServerDomain { + ServerDomain({ + required this.domainName, + required this.zoneId, + required this.provider, + }); + + @HiveField(0) + final String domainName; + + @HiveField(1) + final String zoneId; + + @HiveField(2, defaultValue: DnsProvider.cloudflare) + final DnsProvider provider; + + @override + String toString() => '$domainName: $zoneId'; +} + +@HiveType(typeId: 100) +enum DnsProvider { + @HiveField(0) + unknown, + @HiveField(1) + cloudflare, +} diff --git a/lib/logic/models/hive/server_domain.g.dart b/lib/logic/models/hive/server_domain.g.dart new file mode 100644 index 00000000..3265db6b --- /dev/null +++ b/lib/logic/models/hive/server_domain.g.dart @@ -0,0 +1,87 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server_domain.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class ServerDomainAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + ServerDomain read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ServerDomain( + domainName: fields[0] as String, + zoneId: fields[1] as String, + provider: + fields[2] == null ? DnsProvider.cloudflare : fields[2] as DnsProvider, + ); + } + + @override + void write(BinaryWriter writer, ServerDomain obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.domainName) + ..writeByte(1) + ..write(obj.zoneId) + ..writeByte(2) + ..write(obj.provider); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ServerDomainAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class DnsProviderAdapter extends TypeAdapter { + @override + final int typeId = 100; + + @override + DnsProvider read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return DnsProvider.unknown; + case 1: + return DnsProvider.cloudflare; + default: + return DnsProvider.unknown; + } + } + + @override + void write(BinaryWriter writer, DnsProvider obj) { + switch (obj) { + case DnsProvider.unknown: + writer.writeByte(0); + break; + case DnsProvider.cloudflare: + writer.writeByte(1); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DnsProviderAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/logic/models/user.dart b/lib/logic/models/hive/user.dart similarity index 79% rename from lib/logic/models/user.dart rename to lib/logic/models/hive/user.dart index 8161f61b..942ce9fe 100644 --- a/lib/logic/models/user.dart +++ b/lib/logic/models/hive/user.dart @@ -8,7 +8,7 @@ part 'user.g.dart'; @HiveType(typeId: 1) class User extends Equatable { - User({ + const User({ required this.login, this.password, this.sshKeys = const [], @@ -22,7 +22,7 @@ class User extends Equatable { @HiveField(1) final String? password; - @HiveField(2, defaultValue: const []) + @HiveField(2, defaultValue: []) final List sshKeys; @HiveField(3, defaultValue: true) @@ -36,7 +36,7 @@ class User extends Equatable { Color get color => stringToColor(login); - String toString() { - return '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; - } + @override + String toString() => + '$login, ${isFoundOnServer ? 'found' : 'not found'}, ${sshKeys.length} ssh keys, note: $note'; } diff --git a/lib/logic/models/user.g.dart b/lib/logic/models/hive/user.g.dart similarity index 83% rename from lib/logic/models/user.g.dart rename to lib/logic/models/hive/user.g.dart index a1889dc1..d9b28d65 100644 --- a/lib/logic/models/user.g.dart +++ b/lib/logic/models/hive/user.g.dart @@ -11,9 +11,9 @@ class UserAdapter extends TypeAdapter { final int typeId = 1; @override - User read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { + User read(final BinaryReader reader) { + final int numOfFields = reader.readByte(); + final Map fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return User( @@ -26,7 +26,7 @@ class UserAdapter extends TypeAdapter { } @override - void write(BinaryWriter writer, User obj) { + void write(final BinaryWriter writer, final User obj) { writer ..writeByte(5) ..writeByte(0) @@ -45,7 +45,7 @@ class UserAdapter extends TypeAdapter { int get hashCode => typeId.hashCode; @override - bool operator ==(Object other) => + bool operator ==(final Object other) => identical(this, other) || other is UserAdapter && runtimeType == other.runtimeType && diff --git a/lib/logic/models/job.dart b/lib/logic/models/job.dart index 698cd1fb..b04d7d05 100644 --- a/lib/logic/models/job.dart +++ b/lib/logic/models/job.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/utils/password_generator.dart'; -import 'user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; @immutable class Job extends Equatable { Job({ - String? id, required this.title, + final String? id, }) : id = id ?? StringGenerators.simpleId(); final String title; @@ -45,8 +45,8 @@ class DeleteUserJob extends Job { class ToggleJob extends Job { ToggleJob({ required this.type, - required String title, - }) : super(title: title); + required final super.title, + }); final dynamic type; @@ -56,12 +56,11 @@ class ToggleJob extends Job { class ServiceToggleJob extends ToggleJob { ServiceToggleJob({ - required ServiceTypes type, + required final ServiceTypes super.type, required this.needToTurnOn, }) : super( title: '${needToTurnOn ? "jobs.serviceTurnOn".tr() : "jobs.serviceTurnOff".tr()} ${type.title}', - type: type, ); final bool needToTurnOn; @@ -71,7 +70,7 @@ class CreateSSHKeyJob extends Job { CreateSSHKeyJob({ required this.user, required this.publicKey, - }) : super(title: '${"jobs.create_ssh_key".tr(args: [user.login])}'); + }) : super(title: 'jobs.create_ssh_key'.tr(args: [user.login])); final User user; final String publicKey; @@ -84,7 +83,7 @@ class DeleteSSHKeyJob extends Job { DeleteSSHKeyJob({ required this.user, required this.publicKey, - }) : super(title: '${"jobs.delete_ssh_key".tr(args: [user.login])}'); + }) : super(title: 'jobs.delete_ssh_key'.tr(args: [user.login])); final User user; final String publicKey; diff --git a/lib/logic/models/json/api_token.dart b/lib/logic/models/json/api_token.dart new file mode 100644 index 00000000..980d5132 --- /dev/null +++ b/lib/logic/models/json/api_token.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'api_token.g.dart'; + +@JsonSerializable() +class ApiToken { + factory ApiToken.fromJson(final Map json) => + _$ApiTokenFromJson(json); + ApiToken({ + required this.name, + required this.date, + required this.isCaller, + }); + + final String name; + final DateTime date; + @JsonKey(name: 'is_caller') + final bool isCaller; +} diff --git a/lib/logic/models/json/api_token.g.dart b/lib/logic/models/json/api_token.g.dart new file mode 100644 index 00000000..b6c8b8db --- /dev/null +++ b/lib/logic/models/json/api_token.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ApiToken _$ApiTokenFromJson(Map json) => ApiToken( + name: json['name'] as String, + date: DateTime.parse(json['date'] as String), + isCaller: json['is_caller'] as bool, + ); diff --git a/lib/logic/models/auto_upgrade_settings.dart b/lib/logic/models/json/auto_upgrade_settings.dart similarity index 83% rename from lib/logic/models/auto_upgrade_settings.dart rename to lib/logic/models/json/auto_upgrade_settings.dart index 6007e622..421f9b88 100644 --- a/lib/logic/models/auto_upgrade_settings.dart +++ b/lib/logic/models/json/auto_upgrade_settings.dart @@ -5,18 +5,18 @@ part 'auto_upgrade_settings.g.dart'; @JsonSerializable(createToJson: true) class AutoUpgradeSettings extends Equatable { - final bool enable; - final bool allowReboot; + factory AutoUpgradeSettings.fromJson(final Map json) => + _$AutoUpgradeSettingsFromJson(json); - AutoUpgradeSettings({ + const AutoUpgradeSettings({ required this.enable, required this.allowReboot, }); + final bool enable; + final bool allowReboot; @override List get props => [enable, allowReboot]; - factory AutoUpgradeSettings.fromJson(Map json) => - _$AutoUpgradeSettingsFromJson(json); Map toJson() => _$AutoUpgradeSettingsToJson(this); } diff --git a/lib/logic/models/auto_upgrade_settings.g.dart b/lib/logic/models/json/auto_upgrade_settings.g.dart similarity index 100% rename from lib/logic/models/auto_upgrade_settings.g.dart rename to lib/logic/models/json/auto_upgrade_settings.g.dart diff --git a/lib/logic/models/backup.dart b/lib/logic/models/json/backup.dart similarity index 74% rename from lib/logic/models/backup.dart rename to lib/logic/models/json/backup.dart index 95737897..2e1215db 100644 --- a/lib/logic/models/backup.dart +++ b/lib/logic/models/json/backup.dart @@ -4,14 +4,14 @@ part 'backup.g.dart'; @JsonSerializable() class Backup { + factory Backup.fromJson(final Map json) => + _$BackupFromJson(json); Backup({required this.time, required this.id}); // Time of the backup final DateTime time; @JsonKey(name: 'short_id') final String id; - - factory Backup.fromJson(Map json) => _$BackupFromJson(json); } enum BackupStatusEnum { @@ -33,16 +33,16 @@ enum BackupStatusEnum { @JsonSerializable() class BackupStatus { - BackupStatus( - {required this.status, - required this.progress, - required this.errorMessage}); + factory BackupStatus.fromJson(final Map json) => + _$BackupStatusFromJson(json); + BackupStatus({ + required this.status, + required this.progress, + required this.errorMessage, + }); final BackupStatusEnum status; final double progress; @JsonKey(name: 'error_message') final String? errorMessage; - - factory BackupStatus.fromJson(Map json) => - _$BackupStatusFromJson(json); } diff --git a/lib/logic/models/backup.g.dart b/lib/logic/models/json/backup.g.dart similarity index 100% rename from lib/logic/models/backup.g.dart rename to lib/logic/models/json/backup.g.dart diff --git a/lib/logic/models/json/device_token.dart b/lib/logic/models/json/device_token.dart new file mode 100644 index 00000000..2ec23012 --- /dev/null +++ b/lib/logic/models/json/device_token.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'device_token.g.dart'; + +@JsonSerializable() +class DeviceToken { + factory DeviceToken.fromJson(final Map json) => + _$DeviceTokenFromJson(json); + DeviceToken({ + required this.device, + required this.token, + }); + + final String device; + final String token; +} diff --git a/lib/logic/models/json/device_token.g.dart b/lib/logic/models/json/device_token.g.dart new file mode 100644 index 00000000..efe976c5 --- /dev/null +++ b/lib/logic/models/json/device_token.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'device_token.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeviceToken _$DeviceTokenFromJson(Map json) => DeviceToken( + device: json['device'] as String, + token: json['token'] as String, + ); diff --git a/lib/logic/models/dns_records.dart b/lib/logic/models/json/dns_records.dart similarity index 88% rename from lib/logic/models/dns_records.dart rename to lib/logic/models/json/dns_records.dart index 25aad046..cd4867c3 100644 --- a/lib/logic/models/dns_records.dart +++ b/lib/logic/models/json/dns_records.dart @@ -20,5 +20,5 @@ class DnsRecord { final int priority; final bool proxied; - toJson() => _$DnsRecordToJson(this); + Map toJson() => _$DnsRecordToJson(this); } diff --git a/lib/logic/models/dns_records.g.dart b/lib/logic/models/json/dns_records.g.dart similarity index 100% rename from lib/logic/models/dns_records.g.dart rename to lib/logic/models/json/dns_records.g.dart diff --git a/lib/logic/models/hetzner_server_info.dart b/lib/logic/models/json/hetzner_server_info.dart similarity index 59% rename from lib/logic/models/hetzner_server_info.dart rename to lib/logic/models/json/hetzner_server_info.dart index 98af1c3e..ccf036a1 100644 --- a/lib/logic/models/hetzner_server_info.dart +++ b/lib/logic/models/json/hetzner_server_info.dart @@ -4,23 +4,6 @@ part 'hetzner_server_info.g.dart'; @JsonSerializable() class HetznerServerInfo { - final int id; - final String name; - final ServerStatus status; - final DateTime created; - - @JsonKey(name: 'server_type') - final HetznerServerTypeInfo serverType; - - @JsonKey(name: 'datacenter', fromJson: HetznerServerInfo.locationFromJson) - final HetznerLocation location; - - static HetznerLocation locationFromJson(Map json) => - HetznerLocation.fromJson(json['location']); - - static HetznerServerInfo fromJson(Map json) => - _$HetznerServerInfoFromJson(json); - HetznerServerInfo( this.id, this.name, @@ -28,7 +11,51 @@ class HetznerServerInfo { this.created, this.serverType, this.location, + this.publicNet, + this.volumes, ); + final int id; + final String name; + final ServerStatus status; + final DateTime created; + final List volumes; + + @JsonKey(name: 'server_type') + final HetznerServerTypeInfo serverType; + + @JsonKey(name: 'datacenter', fromJson: HetznerServerInfo.locationFromJson) + final HetznerLocation location; + + @JsonKey(name: 'public_net') + final HetznerPublicNetInfo publicNet; + + static HetznerLocation locationFromJson(final Map json) => + HetznerLocation.fromJson(json['location']); + + static HetznerServerInfo fromJson(final Map json) => + _$HetznerServerInfoFromJson(json); +} + +@JsonSerializable() +class HetznerPublicNetInfo { + HetznerPublicNetInfo(this.ipv4); + final HetznerIp4 ipv4; + + static HetznerPublicNetInfo fromJson(final Map json) => + _$HetznerPublicNetInfoFromJson(json); +} + +@JsonSerializable() +class HetznerIp4 { + HetznerIp4(this.id, this.ip, this.blocked, this.reverseDns); + final bool blocked; + @JsonKey(name: 'dns_ptr') + final String reverseDns; + final int id; + final String ip; + + static HetznerIp4 fromJson(final Map json) => + _$HetznerIp4FromJson(json); } enum ServerStatus { @@ -45,15 +72,14 @@ enum ServerStatus { @JsonSerializable() class HetznerServerTypeInfo { + HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); final int cores; final num memory; final int disk; final List prices; - HetznerServerTypeInfo(this.cores, this.memory, this.disk, this.prices); - - static HetznerServerTypeInfo fromJson(Map json) => + static HetznerServerTypeInfo fromJson(final Map json) => _$HetznerServerTypeInfoFromJson(json); } @@ -67,14 +93,16 @@ class HetznerPriceInfo { @JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice) final double monthly; - static HetznerPriceInfo fromJson(Map json) => + static HetznerPriceInfo fromJson(final Map json) => _$HetznerPriceInfoFromJson(json); - static double getPrice(Map json) => double.parse(json['gross'] as String); + static double getPrice(final Map json) => + double.parse(json['gross'] as String); } @JsonSerializable() class HetznerLocation { + HetznerLocation(this.country, this.city, this.description, this.zone); final String country; final String city; final String description; @@ -82,8 +110,6 @@ class HetznerLocation { @JsonKey(name: 'network_zone') final String zone; - HetznerLocation(this.country, this.city, this.description, this.zone); - - static HetznerLocation fromJson(Map json) => + static HetznerLocation fromJson(final Map json) => _$HetznerLocationFromJson(json); } diff --git a/lib/logic/models/hetzner_server_info.g.dart b/lib/logic/models/json/hetzner_server_info.g.dart similarity index 77% rename from lib/logic/models/hetzner_server_info.g.dart rename to lib/logic/models/json/hetzner_server_info.g.dart index 73e6be68..e8c21917 100644 --- a/lib/logic/models/hetzner_server_info.g.dart +++ b/lib/logic/models/json/hetzner_server_info.g.dart @@ -15,6 +15,8 @@ HetznerServerInfo _$HetznerServerInfoFromJson(Map json) => HetznerServerTypeInfo.fromJson( json['server_type'] as Map), HetznerServerInfo.locationFromJson(json['datacenter'] as Map), + HetznerPublicNetInfo.fromJson(json['public_net'] as Map), + (json['volumes'] as List).map((e) => e as int).toList(), ); const _$ServerStatusEnumMap = { @@ -29,6 +31,19 @@ const _$ServerStatusEnumMap = { ServerStatus.unknown: 'unknown', }; +HetznerPublicNetInfo _$HetznerPublicNetInfoFromJson( + Map json) => + HetznerPublicNetInfo( + HetznerIp4.fromJson(json['ipv4'] as Map), + ); + +HetznerIp4 _$HetznerIp4FromJson(Map json) => HetznerIp4( + json['id'] as int, + json['ip'] as String, + json['blocked'] as bool, + json['dns_ptr'] as String, + ); + HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( Map json) => HetznerServerTypeInfo( diff --git a/lib/logic/models/json/recovery_token_status.dart b/lib/logic/models/json/recovery_token_status.dart new file mode 100644 index 00000000..6e59b57d --- /dev/null +++ b/lib/logic/models/json/recovery_token_status.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'recovery_token_status.g.dart'; + +@JsonSerializable() +class RecoveryKeyStatus extends Equatable { + factory RecoveryKeyStatus.fromJson(final Map json) => + _$RecoveryKeyStatusFromJson(json); + const RecoveryKeyStatus({ + required this.exists, + required this.valid, + this.date, + this.expiration, + this.usesLeft, + }); + + final bool exists; + final DateTime? date; + final DateTime? expiration; + @JsonKey(name: 'uses_left') + final int? usesLeft; + final bool valid; + + @override + List get props => [ + exists, + date, + expiration, + usesLeft, + valid, + ]; +} diff --git a/lib/logic/models/json/recovery_token_status.g.dart b/lib/logic/models/json/recovery_token_status.g.dart new file mode 100644 index 00000000..cef9abbd --- /dev/null +++ b/lib/logic/models/json/recovery_token_status.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'recovery_token_status.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RecoveryKeyStatus _$RecoveryKeyStatusFromJson(Map json) => + RecoveryKeyStatus( + exists: json['exists'] as bool, + valid: json['valid'] as bool, + date: + json['date'] == null ? null : DateTime.parse(json['date'] as String), + expiration: json['expiration'] == null + ? null + : DateTime.parse(json['expiration'] as String), + usesLeft: json['uses_left'] as int?, + ); diff --git a/lib/logic/models/server_configurations.dart b/lib/logic/models/json/server_configurations.dart similarity index 87% rename from lib/logic/models/server_configurations.dart rename to lib/logic/models/json/server_configurations.dart index 73915566..8b4029ab 100644 --- a/lib/logic/models/server_configurations.dart +++ b/lib/logic/models/json/server_configurations.dart @@ -5,6 +5,8 @@ part 'server_configurations.g.dart'; @JsonSerializable(createToJson: true) class AutoUpgradeConfigurations extends Equatable { + factory AutoUpgradeConfigurations.fromJson(final Map json) => + _$AutoUpgradeConfigurationsFromJson(json); const AutoUpgradeConfigurations({ required this.enable, required this.allowReboot, @@ -12,9 +14,6 @@ class AutoUpgradeConfigurations extends Equatable { final bool enable; final bool allowReboot; - - factory AutoUpgradeConfigurations.fromJson(Map json) => - _$AutoUpgradeConfigurationsFromJson(json); Map toJson() => _$AutoUpgradeConfigurationsToJson(this); @override diff --git a/lib/logic/models/server_configurations.g.dart b/lib/logic/models/json/server_configurations.g.dart similarity index 100% rename from lib/logic/models/server_configurations.g.dart rename to lib/logic/models/json/server_configurations.g.dart diff --git a/lib/logic/models/message.dart b/lib/logic/models/message.dart index 79f21a54..8bbc6dfd 100644 --- a/lib/logic/models/message.dart +++ b/lib/logic/models/message.dart @@ -1,19 +1,17 @@ import 'package:intl/intl.dart'; -final formatter = new DateFormat('hh:mm'); +final DateFormat formatter = DateFormat('hh:mm'); class Message { Message({this.text, this.type = MessageType.normal}) : time = DateTime.now(); + Message.warn({this.text}) + : type = MessageType.warning, + time = DateTime.now(); final String? text; final DateTime time; final MessageType type; String get timeString => formatter.format(time); - - static Message warn({String? text}) => Message( - text: text, - type: MessageType.warning, - ); } enum MessageType { diff --git a/lib/logic/models/provider.dart b/lib/logic/models/provider.dart index 82af8656..6feb175b 100644 --- a/lib/logic/models/provider.dart +++ b/lib/logic/models/provider.dart @@ -15,7 +15,7 @@ class ProviderModel extends Equatable { final StateType state; final ProviderType type; - ProviderModel updateState(StateType newState) => ProviderModel( + ProviderModel updateState(final StateType newState) => ProviderModel( state: newState, type: type, ); diff --git a/lib/logic/models/server_basic_info.dart b/lib/logic/models/server_basic_info.dart new file mode 100644 index 00000000..8670dc8c --- /dev/null +++ b/lib/logic/models/server_basic_info.dart @@ -0,0 +1,46 @@ +class ServerBasicInfo { + ServerBasicInfo({ + required this.id, + required this.name, + required this.reverseDns, + required this.ip, + required this.created, + required this.volumeId, + }); + final int id; + final String name; + final String reverseDns; + final String ip; + final DateTime created; + final int volumeId; +} + +class ServerBasicInfoWithValidators extends ServerBasicInfo { + ServerBasicInfoWithValidators.fromServerBasicInfo({ + required final ServerBasicInfo serverBasicInfo, + required final isIpValid, + required final isReverseDnsValid, + }) : this( + id: serverBasicInfo.id, + name: serverBasicInfo.name, + reverseDns: serverBasicInfo.reverseDns, + ip: serverBasicInfo.ip, + created: serverBasicInfo.created, + volumeId: serverBasicInfo.volumeId, + isIpValid: isIpValid, + isReverseDnsValid: isReverseDnsValid, + ); + + ServerBasicInfoWithValidators({ + required final super.id, + required final super.name, + required final super.reverseDns, + required final super.ip, + required final super.created, + required final super.volumeId, + required this.isIpValid, + required this.isReverseDnsValid, + }); + final bool isIpValid; + final bool isReverseDnsValid; +} diff --git a/lib/logic/models/server_details.dart b/lib/logic/models/server_details.dart deleted file mode 100644 index f928ebc8..00000000 --- a/lib/logic/models/server_details.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:hive/hive.dart'; - -part 'server_details.g.dart'; - -@HiveType(typeId: 2) -class HetznerServerDetails { - HetznerServerDetails({ - required this.ip4, - required this.id, - required this.createTime, - required this.dataBase, - required this.apiToken, - this.startTime, - }); - - @HiveField(0) - final String ip4; - - @HiveField(1) - final int id; - - @HiveField(3) - final DateTime? createTime; - - @HiveField(2) - final DateTime? startTime; - - @HiveField(4) - final HetznerDataBase dataBase; - - @HiveField(5) - final String apiToken; - - HetznerServerDetails copyWith({DateTime? startTime}) { - return HetznerServerDetails( - startTime: startTime ?? this.startTime, - createTime: createTime, - id: id, - ip4: ip4, - dataBase: dataBase, - apiToken: apiToken, - ); - } - - String toString() => id.toString(); -} - -@HiveType(typeId: 5) -class HetznerDataBase { - HetznerDataBase({ - required this.id, - required this.name, - }); - - @HiveField(1) - int id; - @HiveField(2) - String name; -} diff --git a/lib/logic/models/server_status.dart b/lib/logic/models/server_status.dart index 1405bde3..e6b15f25 100644 --- a/lib/logic/models/server_status.dart +++ b/lib/logic/models/server_status.dart @@ -1,24 +1,21 @@ class ServerStatus { - final StatusTypes http; - final StatusTypes imap; - final StatusTypes smtp; - ServerStatus({ required this.http, this.imap = StatusTypes.nodata, this.smtp = StatusTypes.nodata, }); + final StatusTypes http; + final StatusTypes imap; + final StatusTypes smtp; - ServerStatus fromJson(Map json) { - return ServerStatus( - http: statusTypeFromNumber(json['http']), - imap: statusTypeFromNumber(json['imap']), - smtp: statusTypeFromNumber(json['smtp']), - ); - } + ServerStatus fromJson(final Map json) => ServerStatus( + http: statusTypeFromNumber(json['http']), + imap: statusTypeFromNumber(json['imap']), + smtp: statusTypeFromNumber(json['smtp']), + ); } -StatusTypes statusTypeFromNumber(int? number) { +StatusTypes statusTypeFromNumber(final int? number) { if (number == 0) { return StatusTypes.ok; } else if (number == 1) { diff --git a/lib/logic/models/timezone_settings.dart b/lib/logic/models/timezone_settings.dart index 76d28aff..22c84b44 100644 --- a/lib/logic/models/timezone_settings.dart +++ b/lib/logic/models/timezone_settings.dart @@ -1,18 +1,15 @@ import 'package:timezone/timezone.dart'; class TimeZoneSettings { - final Location timezone; - - TimeZoneSettings(this.timezone); - - Map toJson() { - return { - 'timezone': timezone.name, - }; - } - - factory TimeZoneSettings.fromString(String string) { - var location = timeZoneDatabase.locations[string]!; + factory TimeZoneSettings.fromString(final String string) { + final Location location = timeZoneDatabase.locations[string]!; return TimeZoneSettings(location); } + + TimeZoneSettings(this.timezone); + final Location timezone; + + Map toJson() => { + 'timezone': timezone.name, + }; } diff --git a/lib/main.dart b/lib/main.dart index e5af3656..f2c36392 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,19 +2,20 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/hive_config.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/theming/factory/app_theme_factory.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:wakelock/wakelock.dart'; import 'package:timezone/data/latest.dart' as tz; -import 'config/bloc_config.dart'; -import 'config/bloc_observer.dart'; -import 'config/brand_theme.dart'; -import 'config/get_it_config.dart'; -import 'config/localization.dart'; -import 'logic/cubit/app_settings/app_settings_cubit.dart'; +import 'package:selfprivacy/config/bloc_config.dart'; +import 'package:selfprivacy/config/bloc_observer.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/config/localization.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -33,22 +34,49 @@ void main() async { await EasyLocalization.ensureInitialized(); tz.initializeTimeZones(); + final ThemeData lightThemeData = await AppThemeFactory.create( + isDark: false, + fallbackColor: BrandColors.primary, + ); + final ThemeData darkThemeData = await AppThemeFactory.create( + isDark: true, + fallbackColor: BrandColors.primary, + ); + BlocOverrides.runZoned( - () => runApp(Localization(child: MyApp())), + () => runApp( + Localization( + child: MyApp( + lightThemeData: lightThemeData, + darkThemeData: darkThemeData, + ), + ), + ), blocObserver: SimpleBlocObserver(), ); } class MyApp extends StatelessWidget { + const MyApp({ + required this.lightThemeData, + required this.darkThemeData, + final super.key, + }); + + final ThemeData lightThemeData; + final ThemeData darkThemeData; + @override - Widget build(BuildContext context) { - return Localization( - child: AnnotatedRegion( - value: SystemUiOverlayStyle.light, // Manually changing appbar color - child: BlocAndProviderConfig( - child: BlocBuilder( - builder: (context, appSettings) { - return MaterialApp( + Widget build(final BuildContext context) => Localization( + child: AnnotatedRegion( + value: SystemUiOverlayStyle.light, // Manually changing appbar color + child: BlocAndProviderConfig( + child: BlocBuilder( + builder: ( + final BuildContext context, + final AppSettingsState appSettings, + ) => + MaterialApp( scaffoldMessengerKey: getIt.get().scaffoldMessengerKey, navigatorKey: getIt.get().navigatorKey, @@ -57,23 +85,25 @@ class MyApp extends StatelessWidget { locale: context.locale, debugShowCheckedModeBanner: false, title: 'SelfPrivacy', - theme: appSettings.isDarkModeOn ? darkTheme : lightTheme, - home: appSettings.isOnbordingShowing - ? OnboardingPage(nextPage: InitializingPage()) - : RootPage(), - builder: (BuildContext context, Widget? widget) { - Widget error = Text('...rendering error...'); - if (widget is Scaffold || widget is Navigator) + theme: lightThemeData, + darkTheme: darkThemeData, + themeMode: + appSettings.isDarkModeOn ? ThemeMode.dark : ThemeMode.light, + home: appSettings.isOnboardingShowing + ? const OnboardingPage(nextPage: InitializingPage()) + : const RootPage(), + builder: (final BuildContext context, final Widget? widget) { + Widget error = const Text('...rendering error...'); + if (widget is Scaffold || widget is Navigator) { error = Scaffold(body: Center(child: error)); + } ErrorWidget.builder = - (FlutterErrorDetails errorDetails) => error; + (final FlutterErrorDetails errorDetails) => error; return widget!; }, - ); - }, + ), + ), ), ), - ), - ); - } + ); } diff --git a/lib/theming/factory/app_theme_factory.dart b/lib/theming/factory/app_theme_factory.dart new file mode 100644 index 00000000..48f4b086 --- /dev/null +++ b/lib/theming/factory/app_theme_factory.dart @@ -0,0 +1,87 @@ +import 'dart:io'; + +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:system_theme/system_theme.dart'; +import 'package:gtk_theme_fl/gtk_theme_fl.dart'; + +abstract class AppThemeFactory { + AppThemeFactory._(); + + static Future create({ + required final bool isDark, + required final Color fallbackColor, + }) => + _createAppTheme( + isDark: isDark, + fallbackColor: fallbackColor, + ); + + static Future _createAppTheme({ + required final Color fallbackColor, + final bool isDark = false, + }) async { + ColorScheme? gtkColorsScheme; + final Brightness brightness = isDark ? Brightness.dark : Brightness.light; + + final ColorScheme? dynamicColorsScheme = + await _getDynamicColors(brightness); + + if (Platform.isLinux) { + final GtkThemeData themeData = await GtkThemeData.initialize(); + final bool isGtkDark = + Color(themeData.theme_bg_color).computeLuminance() < 0.5; + final bool isInverseNeeded = isGtkDark != isDark; + gtkColorsScheme = ColorScheme.fromSeed( + seedColor: Color(themeData.theme_selected_bg_color), + brightness: brightness, + background: isInverseNeeded ? null : Color(themeData.theme_bg_color), + surface: isInverseNeeded ? null : Color(themeData.theme_base_color), + ); + } + + final SystemAccentColor accentColor = SystemAccentColor(fallbackColor); + + try { + await accentColor.load(); + } on MissingPluginException catch (e) { + print('_createAppTheme: ${e.message}'); + } + + final ColorScheme fallbackColorScheme = ColorScheme.fromSeed( + seedColor: accentColor.accent, + brightness: brightness, + ); + + final ColorScheme colorScheme = + dynamicColorsScheme ?? gtkColorsScheme ?? fallbackColorScheme; + + final Typography appTypography = Typography.material2021(); + + final ThemeData materialThemeData = ThemeData( + colorScheme: colorScheme, + brightness: colorScheme.brightness, + typography: appTypography, + useMaterial3: true, + scaffoldBackgroundColor: colorScheme.background, + appBarTheme: AppBarTheme( + elevation: 0, + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + ), + ); + + return materialThemeData; + } + + static Future _getDynamicColors(final Brightness brightness) { + try { + return DynamicColorPlugin.getCorePalette().then( + (final corePallet) => corePallet?.toColorScheme(brightness: brightness), + ); + } on PlatformException { + return Future.value(null); + } + } +} diff --git a/lib/ui/components/action_button/action_button.dart b/lib/ui/components/action_button/action_button.dart index bc0393e7..3a518496 100644 --- a/lib/ui/components/action_button/action_button.dart +++ b/lib/ui/components/action_button/action_button.dart @@ -3,28 +3,28 @@ import 'package:selfprivacy/config/brand_colors.dart'; class ActionButton extends StatelessWidget { const ActionButton({ - Key? key, + final super.key, this.text, this.onPressed, this.isRed = false, - }) : super(key: key); + }); final VoidCallback? onPressed; final String? text; final bool isRed; @override - Widget build(BuildContext context) { - var navigator = Navigator.of(context); + Widget build(final BuildContext context) { + final NavigatorState navigator = Navigator.of(context); return TextButton( child: Text( text!, - style: isRed ? TextStyle(color: BrandColors.red1) : null, + style: isRed ? const TextStyle(color: BrandColors.red1) : null, ), onPressed: () { navigator.pop(); - if (onPressed != null) onPressed!(); + onPressed?.call(); }, ); } diff --git a/lib/ui/components/brand_alert/brand_alert.dart b/lib/ui/components/brand_alert/brand_alert.dart index e4a8f04c..0d673ded 100644 --- a/lib/ui/components/brand_alert/brand_alert.dart +++ b/lib/ui/components/brand_alert/brand_alert.dart @@ -2,14 +2,12 @@ import 'package:flutter/material.dart'; class BrandAlert extends AlertDialog { BrandAlert({ - Key? key, - String? title, - String? contentText, - List? actions, + final super.key, + final String? title, + final String? contentText, + final super.actions, }) : super( - key: key, title: title != null ? Text(title) : null, content: title != null ? Text(contentText!) : null, - actions: actions, ); } diff --git a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart index d5718181..de322b05 100644 --- a/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart +++ b/lib/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart @@ -3,17 +3,17 @@ import 'package:selfprivacy/config/brand_colors.dart'; class BrandBottomSheet extends StatelessWidget { const BrandBottomSheet({ - Key? key, required this.child, + final super.key, this.isExpended = false, - }) : super(key: key); + }); final Widget child; final bool isExpended; @override - Widget build(BuildContext context) { - var mainHeight = MediaQuery.of(context).size.height - + Widget build(final BuildContext context) { + final double mainHeight = MediaQuery.of(context).size.height - MediaQuery.of(context).padding.top - 100; late Widget innerWidget; @@ -44,9 +44,9 @@ class BrandBottomSheet extends StatelessWidget { ), ), ), - SizedBox(height: 6), + const SizedBox(height: 6), ClipRRect( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), child: ConstrainedBox( constraints: BoxConstraints(maxHeight: mainHeight), child: innerWidget, diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 86020121..8951b70f 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -1,44 +1,52 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; enum BrandButtonTypes { rised, text, iconText } class BrandButton { - static rised({ - Key? key, - required VoidCallback? onPressed, - String? text, - Widget? child, + static ConstrainedBox rised({ + required final VoidCallback? onPressed, + final Key? key, + final String? text, + final Widget? child, }) { assert(text == null || child == null, 'required title or child'); assert(text != null || child != null, 'required title or child'); - return _RisedButton( - key: key, - title: text, - onPressed: onPressed, - child: child, + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 48, + minWidth: double.infinity, + ), + child: FilledButton( + key: key, + title: text, + onPressed: onPressed, + child: child, + ), ); } - static text({ - Key? key, - required VoidCallback onPressed, - required String title, + static ConstrainedBox text({ + required final VoidCallback onPressed, + required final String title, + final Key? key, }) => - _TextButton( - key: key, - title: title, - onPressed: onPressed, + ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + minWidth: double.infinity, + ), + child: TextButton(onPressed: onPressed, child: Text(title)), ); - static emptyWithIconText({ - Key? key, - required VoidCallback onPressed, - required String title, - required Icon icon, + static IconTextButton emptyWithIconText({ + required final VoidCallback onPressed, + required final String title, + required final Icon icon, + final Key? key, }) => - _IconTextButton( + IconTextButton( key: key, title: title, onPressed: onPressed, @@ -46,108 +54,38 @@ class BrandButton { ); } -class _RisedButton extends StatelessWidget { - const _RisedButton({ - Key? key, +class IconTextButton extends StatelessWidget { + const IconTextButton({ + final super.key, this.onPressed, this.title, - this.child, - }) : super(key: key); - - final VoidCallback? onPressed; - final String? title; - final Widget? child; - - @override - Widget build(BuildContext context) { - return ClipRRect( - borderRadius: BorderRadius.circular(24), - child: ColoredBox( - color: onPressed == null - ? BrandColors.gray2 - : Theme.of(context).primaryColor, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - child: Container( - height: 48, - width: double.infinity, - alignment: Alignment.center, - padding: EdgeInsets.all(12), - child: child ?? BrandText.buttonTitleText(title), - ), - ), - ), - ), - ); - } -} - -class _TextButton extends StatelessWidget { - const _TextButton({ - Key? key, - this.onPressed, - this.title, - }) : super(key: key); - - final VoidCallback? onPressed; - final String? title; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onPressed, - behavior: HitTestBehavior.opaque, - child: Container( - height: 48, - width: double.infinity, - alignment: Alignment.center, - padding: EdgeInsets.all(12), - child: Text( - title!, - style: TextStyle( - color: BrandColors.blue, - fontSize: 16, - fontWeight: FontWeight.bold, - height: 1.5, - ), - ), - ), - ); - } -} - -class _IconTextButton extends StatelessWidget { - const _IconTextButton({Key? key, this.onPressed, this.title, this.icon}) - : super(key: key); + this.icon, + }); final VoidCallback? onPressed; final String? title; final Icon? icon; @override - Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onPressed, - child: Container( - height: 48, - width: double.infinity, - alignment: Alignment.center, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BrandText.body1(title), - Padding( - padding: const EdgeInsets.all(12.0), - child: icon, - ) - ], + Widget build(final BuildContext context) => Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + child: Container( + height: 48, + width: double.infinity, + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BrandText.body1(title), + Padding( + padding: const EdgeInsets.all(12.0), + child: icon, + ) + ], + ), ), ), - ), - ); - } + ); } diff --git a/lib/ui/components/brand_button/filled_button.dart b/lib/ui/components/brand_button/filled_button.dart new file mode 100644 index 00000000..b3888f3c --- /dev/null +++ b/lib/ui/components/brand_button/filled_button.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +class FilledButton extends StatelessWidget { + const FilledButton({ + final super.key, + this.onPressed, + this.title, + this.child, + this.disabled = false, + }); + + final VoidCallback? onPressed; + final String? title; + final Widget? child; + final bool disabled; + + @override + Widget build(final BuildContext context) { + final ButtonStyle enabledStyle = ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onPrimary, + primary: Theme.of(context).colorScheme.primary, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); + + final ButtonStyle disabledStyle = ElevatedButton.styleFrom( + onPrimary: Theme.of(context).colorScheme.onSurface.withAlpha(30), + primary: Theme.of(context).colorScheme.onSurface.withAlpha(98), + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)); + + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40, + minWidth: double.infinity, + ), + child: ElevatedButton( + onPressed: onPressed, + style: disabled ? disabledStyle : enabledStyle, + child: child ?? Text(title ?? ''), + ), + ); + } +} diff --git a/lib/ui/components/brand_cards/brand_cards.dart b/lib/ui/components/brand_cards/brand_cards.dart index 398e7e89..d8f48088 100644 --- a/lib/ui/components/brand_cards/brand_cards.dart +++ b/lib/ui/components/brand_cards/brand_cards.dart @@ -1,38 +1,44 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; class BrandCards { - static Widget big({required Widget child}) => _BrandCard( - child: child, - padding: EdgeInsets.symmetric( + static Widget big({required final Widget child}) => _BrandCard( + padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 15, ), shadow: bigShadow, borderRadius: BorderRadius.circular(20), - ); - static Widget small({required Widget child}) => _BrandCard( child: child, - padding: EdgeInsets.symmetric( + ); + static Widget small({required final Widget child}) => _BrandCard( + padding: const EdgeInsets.symmetric( horizontal: 15, vertical: 10, ), shadow: bigShadow, borderRadius: BorderRadius.circular(10), + child: child, ); - static Widget outlined({required Widget child}) => _OutlinedCard( + static Widget outlined({required final Widget child}) => _OutlinedCard( + child: child, + ); + static Widget filled({ + required final Widget child, + final bool tertiary = false, + }) => + _FilledCard( + tertiary: tertiary, child: child, ); } class _BrandCard extends StatelessWidget { const _BrandCard({ - Key? key, required this.child, required this.padding, required this.shadow, required this.borderRadius, - }) : super(key: key); + }); final Widget child; final EdgeInsets padding; @@ -40,47 +46,62 @@ class _BrandCard extends StatelessWidget { final BorderRadius borderRadius; @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.black - : BrandColors.white, - borderRadius: borderRadius, - boxShadow: shadow, - ), - padding: padding, - child: child, - ); - } + Widget build(final BuildContext context) => Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: borderRadius, + boxShadow: shadow, + ), + padding: padding, + child: child, + ); } class _OutlinedCard extends StatelessWidget { const _OutlinedCard({ - Key? key, required this.child, - }) : super(key: key); + }); final Widget child; @override - Widget build(BuildContext context) { - return Card( - elevation: 0.0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - side: BorderSide( - color: Colors.grey.withOpacity(0.2), - width: 1, + Widget build(final BuildContext context) => Card( + elevation: 0.0, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), ), - ), - child: child, - ); - } + clipBehavior: Clip.antiAlias, + child: child, + ); } -final bigShadow = [ +class _FilledCard extends StatelessWidget { + const _FilledCard({ + required this.child, + required this.tertiary, + }); + + final Widget child; + final bool tertiary; + @override + Widget build(final BuildContext context) => Card( + elevation: 0.0, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + clipBehavior: Clip.antiAlias, + color: tertiary + ? Theme.of(context).colorScheme.tertiaryContainer + : Theme.of(context).colorScheme.surfaceVariant, + child: child, + ); +} + +final List bigShadow = [ BoxShadow( - offset: Offset(0, 4), + offset: const Offset(0, 4), blurRadius: 8, color: Colors.black.withOpacity(.08), ) diff --git a/lib/ui/components/brand_divider/brand_divider.dart b/lib/ui/components/brand_divider/brand_divider.dart index bd3d9c92..03e44653 100644 --- a/lib/ui/components/brand_divider/brand_divider.dart +++ b/lib/ui/components/brand_divider/brand_divider.dart @@ -2,14 +2,12 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; class BrandDivider extends StatelessWidget { - const BrandDivider({Key? key}) : super(key: key); + const BrandDivider({final super.key}); @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - height: 1, - color: BrandColors.dividerColor, - ); - } + Widget build(final BuildContext context) => Container( + width: double.infinity, + height: 1, + color: BrandColors.dividerColor, + ); } diff --git a/lib/ui/components/brand_header/brand_header.dart b/lib/ui/components/brand_header/brand_header.dart index f9613e08..7366298b 100644 --- a/lib/ui/components/brand_header/brand_header.dart +++ b/lib/ui/components/brand_header/brand_header.dart @@ -1,44 +1,39 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons.dart'; class BrandHeader extends StatelessWidget { const BrandHeader({ - Key? key, - required this.title, + final super.key, + this.title = '', this.hasBackButton = false, - this.hasFlashButton = false, - }) : super(key: key); + this.onBackButtonPressed, + }); final String title; final bool hasBackButton; - final bool hasFlashButton; + final VoidCallback? onBackButtonPressed; @override - Widget build(BuildContext context) { - return Container( - height: 52, - alignment: Alignment.centerLeft, - padding: EdgeInsets.only( - left: hasBackButton ? 1 : 15, - ), - child: Container( + Widget build(final BuildContext context) => Container( + height: 52, + alignment: Alignment.centerLeft, + padding: EdgeInsets.only( + left: hasBackButton ? 1 : 15, + ), child: Row( children: [ if (hasBackButton) ...[ IconButton( - icon: Icon(BrandIcons.arrow_left), - onPressed: () => Navigator.of(context).pop(), + icon: const Icon(BrandIcons.arrowLeft), + onPressed: + onBackButtonPressed ?? () => Navigator.of(context).pop(), ), - SizedBox(width: 10), + const SizedBox(width: 10), ], BrandText.h4(title), - Spacer(), - if (hasFlashButton) PreStyledButtons.flash(), + const Spacer(), ], ), - ), - ); - } + ); } diff --git a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart index 934c952f..cdccc8d2 100644 --- a/lib/ui/components/brand_hero_screen/brand_hero_screen.dart +++ b/lib/ui/components/brand_hero_screen/brand_hero_screen.dart @@ -1,17 +1,19 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; +import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; class BrandHeroScreen extends StatelessWidget { const BrandHeroScreen({ - Key? key, + required this.children, + final super.key, this.headerTitle = '', this.hasBackButton = true, this.hasFlashButton = true, - required this.children, this.heroIcon, this.heroTitle, this.heroSubtitle, - }) : super(key: key); + this.onBackButtonPressed, + }); final List children; final String headerTitle; @@ -20,42 +22,53 @@ class BrandHeroScreen extends StatelessWidget { final IconData? heroIcon; final String? heroTitle; final String? heroSubtitle; + final VoidCallback? onBackButtonPressed; @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(52.0), - child: BrandHeader( - title: headerTitle, - hasBackButton: hasBackButton, - hasFlashButton: hasFlashButton, + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52.0), + child: BrandHeader( + title: headerTitle, + hasBackButton: hasBackButton, + onBackButtonPressed: onBackButtonPressed, + ), + ), + floatingActionButton: hasFlashButton ? const BrandFab() : null, + body: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + if (heroIcon != null) + Container( + alignment: Alignment.bottomLeft, + child: Icon( + heroIcon, + size: 48.0, + ), + ), + const SizedBox(height: 8.0), + if (heroTitle != null) + Text( + heroTitle!, + style: Theme.of(context).textTheme.headlineMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: TextAlign.start, + ), + const SizedBox(height: 8.0), + if (heroSubtitle != null) + Text( + heroSubtitle!, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground, + ), + textAlign: TextAlign.start, + ), + const SizedBox(height: 16.0), + ...children, + ], ), ), - body: ListView( - padding: EdgeInsets.all(16.0), - children: [ - if (heroIcon != null) - Icon( - heroIcon, - size: 48.0, - ), - SizedBox(height: 16.0), - if (heroTitle != null) - Text(heroTitle!, - style: Theme.of(context).textTheme.headline2, - textAlign: TextAlign.center), - SizedBox(height: 8.0), - if (heroSubtitle != null) - Text(heroSubtitle!, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center), - SizedBox(height: 16.0), - ...children, - ], - ), - ), - ); - } + ); } diff --git a/lib/ui/components/brand_icons/brand_icons.dart b/lib/ui/components/brand_icons/brand_icons.dart index b66f8bf6..f66ed8ec 100644 --- a/lib/ui/components/brand_icons/brand_icons.dart +++ b/lib/ui/components/brand_icons/brand_icons.dart @@ -18,7 +18,7 @@ import 'package:flutter/widgets.dart'; class BrandIcons { BrandIcons._(); - static const _kFontFam = 'BrandIcons'; + static const String _kFontFam = 'BrandIcons'; static const String? _kFontPkg = null; static const IconData connection = @@ -67,7 +67,7 @@ class BrandIcons { IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData upload = IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static const IconData arrow_left = + static const IconData arrowLeft = IconData(0xe818, fontFamily: _kFontFam, fontPackage: _kFontPkg); static const IconData shape = IconData(0xe819, fontFamily: _kFontFam, fontPackage: _kFontPkg); diff --git a/lib/ui/components/brand_loader/brand_loader.dart b/lib/ui/components/brand_loader/brand_loader.dart index 9cd5b571..59f1f177 100644 --- a/lib/ui/components/brand_loader/brand_loader.dart +++ b/lib/ui/components/brand_loader/brand_loader.dart @@ -2,19 +2,19 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; class BrandLoader { - static horizontal() => _HorizontalLoader(); + static HorizontalLoader horizontal() => const HorizontalLoader(); } -class _HorizontalLoader extends StatelessWidget { +class HorizontalLoader extends StatelessWidget { + const HorizontalLoader({final super.key}); + @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('basis.wait'.tr()), - SizedBox(height: 10), - LinearProgressIndicator(minHeight: 3), - ], - ); - } + Widget build(final BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('basis.wait'.tr()), + const SizedBox(height: 10), + const LinearProgressIndicator(minHeight: 3), + ], + ); } diff --git a/lib/ui/components/brand_md/brand_md.dart b/lib/ui/components/brand_md/brand_md.dart index 230b5619..249895a9 100644 --- a/lib/ui/components/brand_md/brand_md.dart +++ b/lib/ui/components/brand_md/brand_md.dart @@ -4,18 +4,18 @@ import 'package:flutter/services.dart' show rootBundle; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class BrandMarkdown extends StatefulWidget { const BrandMarkdown({ - Key? key, required this.fileName, - }) : super(key: key); + final super.key, + }); final String fileName; @override - _BrandMarkdownState createState() => _BrandMarkdownState(); + State createState() => _BrandMarkdownState(); } class _BrandMarkdownState extends State { @@ -28,7 +28,7 @@ class _BrandMarkdownState extends State { } void _loadMdFile() async { - String mdFromFile = await rootBundle + final String mdFromFile = await rootBundle .loadString('assets/markdown/${widget.fileName}-${'locale'.tr()}.md'); setState(() { _mdContent = mdFromFile; @@ -36,9 +36,9 @@ class _BrandMarkdownState extends State { } @override - Widget build(BuildContext context) { - var isDark = Theme.of(context).brightness == Brightness.dark; - var markdown = MarkdownStyleSheet( + Widget build(final BuildContext context) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + final MarkdownStyleSheet markdown = MarkdownStyleSheet( p: defaultTextStyle.copyWith( color: isDark ? BrandColors.white : null, ), @@ -55,14 +55,14 @@ class _BrandMarkdownState extends State { color: isDark ? BrandColors.white : null, ), ); - return Markdown( + return MarkdownBody( shrinkWrap: true, styleSheet: markdown, - onTapLink: (String text, String? href, String title) { + onTapLink: (final String text, final String? href, final String title) { if (href != null) { - canLaunch(href).then((canLaunchURL) { + canLaunchUrlString(href).then((final bool canLaunchURL) { if (canLaunchURL) { - launch(href); + launchUrlString(href); } }); } diff --git a/lib/ui/components/brand_radio/brand_radio.dart b/lib/ui/components/brand_radio/brand_radio.dart index 3ae64bcf..60f41fb5 100644 --- a/lib/ui/components/brand_radio/brand_radio.dart +++ b/lib/ui/components/brand_radio/brand_radio.dart @@ -2,41 +2,37 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; class BrandRadio extends StatelessWidget { - BrandRadio({ - Key? key, + const BrandRadio({ required this.isChecked, - }) : super(key: key); + final super.key, + }); final bool isChecked; @override - Widget build(BuildContext context) { - return Container( - height: 20, - width: 20, - alignment: Alignment.center, - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: _getBorder(), - ), - child: isChecked - ? Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: BrandColors.primary, - ), - ) - : null, - ); - } + Widget build(final BuildContext context) => Container( + height: 20, + width: 20, + alignment: Alignment.center, + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: _getBorder(), + ), + child: isChecked + ? Container( + height: 10, + width: 10, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: BrandColors.primary, + ), + ) + : null, + ); - BoxBorder? _getBorder() { - return Border.all( - color: isChecked ? BrandColors.primary : BrandColors.gray1, - width: 2, - ); - } + BoxBorder? _getBorder() => Border.all( + color: isChecked ? BrandColors.primary : BrandColors.gray1, + width: 2, + ); } diff --git a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart index 4f979a47..5b18247d 100644 --- a/lib/ui/components/brand_radio_tile/brand_radio_tile.dart +++ b/lib/ui/components/brand_radio_tile/brand_radio_tile.dart @@ -4,11 +4,11 @@ import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class BrandRadioTile extends StatelessWidget { const BrandRadioTile({ - Key? key, required this.isChecked, required this.text, required this.onPress, - }) : super(key: key); + final super.key, + }); final bool isChecked; @@ -16,22 +16,20 @@ class BrandRadioTile extends StatelessWidget { final VoidCallback onPress; @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onPress, - behavior: HitTestBehavior.translucent, - child: Padding( - padding: EdgeInsets.all(2), - child: Row( - children: [ - BrandRadio( - isChecked: isChecked, - ), - SizedBox(width: 9), - BrandText.h5(text) - ], + Widget build(final BuildContext context) => GestureDetector( + onTap: onPress, + behavior: HitTestBehavior.translucent, + child: Padding( + padding: const EdgeInsets.all(2), + child: Row( + children: [ + BrandRadio( + isChecked: isChecked, + ), + const SizedBox(width: 9), + BrandText.h5(text) + ], + ), ), - ), - ); - } + ); } diff --git a/lib/ui/components/brand_span_button/brand_span_button.dart b/lib/ui/components/brand_span_button/brand_span_button.dart index d6cdcb02..de19730e 100644 --- a/lib/ui/components/brand_span_button/brand_span_button.dart +++ b/lib/ui/components/brand_span_button/brand_span_button.dart @@ -5,29 +5,29 @@ import 'package:url_launcher/url_launcher.dart'; class BrandSpanButton extends TextSpan { BrandSpanButton({ - required String text, - required VoidCallback onTap, - TextStyle? style, + required final String text, + required final VoidCallback onTap, + final TextStyle? style, }) : super( recognizer: TapGestureRecognizer()..onTap = onTap, text: text, - style: (style ?? TextStyle()).copyWith(color: BrandColors.blue), + style: (style ?? const TextStyle()).copyWith(color: BrandColors.blue), ); - static link({ - required String text, - String? urlString, - TextStyle? style, - }) => - BrandSpanButton( - text: text, - style: style, - onTap: () => _launchURL(urlString ?? text), - ); + BrandSpanButton.link({ + required final String text, + final String? urlString, + final TextStyle? style, + }) : super( + recognizer: TapGestureRecognizer() + ..onTap = () => _launchURL(urlString ?? text), + text: text, + style: (style ?? const TextStyle()).copyWith(color: BrandColors.blue), + ); - static _launchURL(String link) async { - if (await canLaunch(link)) { - await launch(link); + static Future _launchURL(final String link) async { + if (await canLaunchUrl(Uri.parse(link))) { + await launchUrl(Uri.parse(link)); } else { throw 'Could not launch $link'; } diff --git a/lib/ui/components/brand_switch/brand_switch.dart b/lib/ui/components/brand_switch/brand_switch.dart index 60c411cf..89396acc 100644 --- a/lib/ui/components/brand_switch/brand_switch.dart +++ b/lib/ui/components/brand_switch/brand_switch.dart @@ -1,24 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; - class BrandSwitch extends StatelessWidget { const BrandSwitch({ - Key? key, required this.onChanged, required this.value, - }) : super(key: key); + final super.key, + }); final ValueChanged onChanged; final bool value; @override - Widget build(BuildContext context) { - return Switch( - activeColor: BrandColors.green1, - activeTrackColor: BrandColors.green2, - value: value, - onChanged: onChanged, - ); - } + Widget build(final BuildContext context) => Switch( + activeColor: Theme.of(context).colorScheme.primary, + value: value, + onChanged: onChanged, + ); } diff --git a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart index 3c544331..194c0ac1 100644 --- a/lib/ui/components/brand_tab_bar/brand_tab_bar.dart +++ b/lib/ui/components/brand_tab_bar/brand_tab_bar.dart @@ -1,16 +1,13 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -final _kBottomTabBarHeight = 51; - class BrandTabBar extends StatefulWidget { - BrandTabBar({Key? key, this.controller}) : super(key: key); + const BrandTabBar({final super.key, this.controller}); final TabController? controller; @override - _BrandTabBarState createState() => _BrandTabBarState(); + State createState() => _BrandTabBarState(); } class _BrandTabBarState extends State { @@ -22,7 +19,7 @@ class _BrandTabBarState extends State { super.initState(); } - _listener() { + void _listener() { if (currentIndex != widget.controller!.index) { setState(() { currentIndex = widget.controller!.index; @@ -37,67 +34,27 @@ class _BrandTabBarState extends State { } @override - Widget build(BuildContext context) { - final paddingBottom = MediaQuery.of(context).padding.bottom; + Widget build(final BuildContext context) => NavigationBar( + destinations: [ + _getIconButton('basis.providers'.tr(), BrandIcons.server, 0), + _getIconButton('basis.services'.tr(), BrandIcons.box, 1), + _getIconButton('basis.users'.tr(), BrandIcons.users, 2), + _getIconButton('basis.more'.tr(), Icons.menu_rounded, 3), + ], + onDestinationSelected: (final index) { + widget.controller!.animateTo(index); + }, + selectedIndex: currentIndex ?? 0, + labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, + ); - return SizedBox( - height: paddingBottom + _kBottomTabBarHeight, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 16), - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.navBackgroundDark - : BrandColors.navBackgroundLight, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _getIconButton('basis.providers'.tr(), BrandIcons.server, 0), - _getIconButton('basis.services'.tr(), BrandIcons.box, 1), - _getIconButton('basis.users'.tr(), BrandIcons.users, 2), - _getIconButton('basis.more'.tr(), BrandIcons.menu, 3), - ], - ), - ), - ); - } - - _getIconButton(String label, IconData iconData, int index) { - var activeColor = Theme.of(context).brightness == Brightness.dark - ? BrandColors.white - : BrandColors.black; - - var isActive = currentIndex == index; - var color = isActive ? activeColor : BrandColors.inactive; - return InkWell( - onTap: () => widget.controller!.animateTo(index), - child: Padding( - padding: EdgeInsets.all(6), - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: 40), - child: Column( - children: [ - Icon(iconData, color: color), - SizedBox(height: 3), - Row( - children: [ - if (isActive) ...[ - Container( - height: 5, - width: 5, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: BrandColors.red2, - ), - ), - SizedBox(width: 5), - ], - Text(label, style: TextStyle(fontSize: 9, color: color)), - ], - ) - ], - ), - ), - ), - ); - } + NavigationDestination _getIconButton( + final String label, + final IconData iconData, + final int index, + ) => + NavigationDestination( + icon: Icon(iconData), + label: label, + ); } diff --git a/lib/ui/components/brand_text/brand_text.dart b/lib/ui/components/brand_text/brand_text.dart index 41436cc1..544ffcec 100644 --- a/lib/ui/components/brand_text/brand_text.dart +++ b/lib/ui/components/brand_text/brand_text.dart @@ -18,66 +18,10 @@ enum TextType { } class BrandText extends StatelessWidget { - const BrandText( - this.text, { - Key? key, - this.style, - required this.type, - this.overflow, - this.softWrap, - this.textAlign, - this.maxLines, - }) : super(key: key); - - final String? text; - final TextStyle? style; - final TextType type; - final TextOverflow? overflow; - final bool? softWrap; - final TextAlign? textAlign; - final int? maxLines; - - factory BrandText.h1( - String? text, { - TextStyle? style, - TextOverflow? overflow, - bool? softWrap, - }) => - BrandText( - text, - type: TextType.h1, - style: style, - ); - - factory BrandText.onboardingTitle(String text, {TextStyle? style}) => - BrandText( - text, - type: TextType.onboardingTitle, - style: style, - ); - factory BrandText.h2( - String? text, { - TextStyle? style, - TextAlign? textAlign, - }) => - BrandText( - text, - type: TextType.h2, - style: style, - textAlign: textAlign, - ); - factory BrandText.h3(String text, {TextStyle? style, TextAlign? textAlign}) => - BrandText( - text, - type: TextType.h3, - style: style, - textAlign: textAlign, - overflow: TextOverflow.ellipsis, - ); factory BrandText.h4( - String? text, { - TextStyle? style, - TextAlign? textAlign, + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, }) => BrandText( text, @@ -89,10 +33,32 @@ class BrandText extends StatelessWidget { textAlign: textAlign, ); + factory BrandText.onboardingTitle( + final String text, { + final TextStyle? style, + }) => + BrandText( + text, + type: TextType.onboardingTitle, + style: style, + ); + factory BrandText.h3( + final String text, { + final TextStyle? style, + final TextAlign? textAlign, + }) => + BrandText( + text, + type: TextType.h3, + style: style, + textAlign: textAlign, + overflow: TextOverflow.ellipsis, + ); + factory BrandText.h4Underlined( - String? text, { - TextStyle? style, - TextAlign? textAlign, + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, }) => BrandText( text, @@ -104,10 +70,60 @@ class BrandText extends StatelessWidget { textAlign: textAlign, ); + factory BrandText.h1( + final String? text, { + final TextStyle? style, + final TextOverflow? overflow, + final bool? softWrap, + }) => + BrandText( + text, + type: TextType.h1, + style: style, + ); + factory BrandText.h2( + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, + }) => + BrandText( + text, + type: TextType.h2, + style: style, + textAlign: textAlign, + ); + factory BrandText.body1(final String? text, {final TextStyle? style}) => + BrandText( + text, + type: TextType.body1, + style: style, + ); + factory BrandText.small(final String text, {final TextStyle? style}) => + BrandText( + text, + type: TextType.small, + style: style, + ); + factory BrandText.body2(final String? text, {final TextStyle? style}) => + BrandText( + text, + type: TextType.body2, + style: style, + ); + factory BrandText.buttonTitleText( + final String? text, { + final TextStyle? style, + }) => + BrandText( + text, + type: TextType.buttonTitleText, + style: style, + ); + factory BrandText.h5( - String? text, { - TextStyle? style, - TextAlign? textAlign, + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, }) => BrandText( text, @@ -115,39 +131,39 @@ class BrandText extends StatelessWidget { style: style, textAlign: textAlign, ); - factory BrandText.body1(String? text, {TextStyle? style}) => BrandText( - text, - type: TextType.body1, - style: style, - ); - factory BrandText.body2(String? text, {TextStyle? style}) => BrandText( - text, - type: TextType.body2, - style: style, - ); - factory BrandText.medium(String? text, - {TextStyle? style, TextAlign? textAlign}) => + factory BrandText.medium( + final String? text, { + final TextStyle? style, + final TextAlign? textAlign, + }) => BrandText( text, type: TextType.medium, style: style, textAlign: textAlign, ); - factory BrandText.small(String text, {TextStyle? style}) => BrandText( - text, - type: TextType.small, - style: style, - ); - factory BrandText.buttonTitleText(String? text, {TextStyle? style}) => - BrandText( - text, - type: TextType.buttonTitleText, - style: style, - ); + const BrandText( + this.text, { + required this.type, + final super.key, + this.style, + this.overflow, + this.softWrap, + this.textAlign, + this.maxLines, + }); + + final String? text; + final TextStyle? style; + final TextType type; + final TextOverflow? overflow; + final bool? softWrap; + final TextAlign? textAlign; + final int? maxLines; @override - Text build(BuildContext context) { + Text build(final BuildContext context) { TextStyle style; - var isDark = Theme.of(context).brightness == Brightness.dark; + final bool isDark = Theme.of(context).brightness == Brightness.dark; switch (type) { case TextType.h1: style = isDark diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index b82df99b..5d76d57d 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -7,16 +7,16 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; class BrandTimer extends StatefulWidget { const BrandTimer({ - Key? key, required this.startDateTime, required this.duration, - }) : super(key: key); + final super.key, + }); final DateTime startDateTime; final Duration duration; @override - _BrandTimerState createState() => _BrandTimerState(); + State createState() => _BrandTimerState(); } class _BrandTimerState extends State { @@ -29,10 +29,11 @@ class _BrandTimerState extends State { super.initState(); } - _timerStart() { + void _timerStart() { _timeString = differenceFromStart; - timer = Timer.periodic(Duration(seconds: 1), (Timer t) { - var timePassed = DateTime.now().difference(widget.startDateTime); + timer = Timer.periodic(const Duration(seconds: 1), (final Timer t) { + final Duration timePassed = + DateTime.now().difference(widget.startDateTime); if (timePassed > widget.duration) { t.cancel(); } else { @@ -42,7 +43,7 @@ class _BrandTimerState extends State { } @override - void didUpdateWidget(BrandTimer oldWidget) { + void didUpdateWidget(final BrandTimer oldWidget) { if (timer.isActive) { timer.cancel(); } @@ -51,14 +52,12 @@ class _BrandTimerState extends State { } @override - Widget build(BuildContext context) { - return BrandText.medium( - _timeString, - style: TextStyle( - fontWeight: NamedFontWeight.demiBold, - ), - ); - } + Widget build(final BuildContext context) => BrandText.medium( + _timeString, + style: const TextStyle( + fontWeight: NamedFontWeight.demiBold, + ), + ); void _getTime() { setState(() { @@ -69,12 +68,12 @@ class _BrandTimerState extends State { String get differenceFromStart => _durationToString(DateTime.now().difference(widget.startDateTime)); - String _durationToString(Duration duration) { - var timeLeft = widget.duration - duration; - String twoDigits(int n) => n.toString().padLeft(2, "0"); - String twoDigitSeconds = twoDigits(timeLeft.inSeconds); + String _durationToString(final Duration duration) { + final Duration timeLeft = widget.duration - duration; + String twoDigits(final int n) => n.toString().padLeft(2, '0'); + final String twoDigitSeconds = twoDigits(timeLeft.inSeconds); - return "timer.sec".tr(args: [twoDigitSeconds]); + return 'timer.sec'.tr(args: [twoDigitSeconds]); } @override diff --git a/lib/ui/components/dots_indicator/dots_indicator.dart b/lib/ui/components/dots_indicator/dots_indicator.dart index ccf42aa5..fff647b7 100644 --- a/lib/ui/components/dots_indicator/dots_indicator.dart +++ b/lib/ui/components/dots_indicator/dots_indicator.dart @@ -3,20 +3,20 @@ import 'package:selfprivacy/config/brand_colors.dart'; class DotsIndicator extends StatelessWidget { const DotsIndicator({ - Key? key, required this.activeIndex, required this.count, - }) : super(key: key); + final super.key, + }); final int activeIndex; final int count; @override - Widget build(BuildContext context) { - var dots = List.generate( + Widget build(final BuildContext context) { + final List dots = List.generate( count, - (index) => Container( - margin: EdgeInsets.symmetric(horizontal: 5, vertical: 10), + (final index) => Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 10), height: 10, width: 10, decoration: BoxDecoration( diff --git a/lib/ui/components/error/error.dart b/lib/ui/components/error/error.dart index ed46f547..d12af1a3 100644 --- a/lib/ui/components/error/error.dart +++ b/lib/ui/components/error/error.dart @@ -1,28 +1,26 @@ import 'package:flutter/material.dart'; class BrandError extends StatelessWidget { - const BrandError({Key? key, this.error, this.stackTrace}) : super(key: key); + const BrandError({final super.key, this.error, this.stackTrace}); final Object? error; final StackTrace? stackTrace; @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - body: Center( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(error.toString()), - Text('stackTrace: '), - Text(stackTrace.toString()), - ], + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + body: Center( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(error.toString()), + const Text('stackTrace: '), + Text(stackTrace.toString()), + ], + ), ), ), ), - ), - ); - } + ); } diff --git a/lib/ui/components/icon_status_mask/icon_status_mask.dart b/lib/ui/components/icon_status_mask/icon_status_mask.dart index cf9fd1d7..0c507ede 100644 --- a/lib/ui/components/icon_status_mask/icon_status_mask.dart +++ b/lib/ui/components/icon_status_mask/icon_status_mask.dart @@ -3,29 +3,36 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; class IconStatusMask extends StatelessWidget { - IconStatusMask({required this.child, required this.status}); + const IconStatusMask({ + required this.child, + required this.status, + final super.key, + }); final Icon child; final StateType status; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { late List colors; switch (status) { case StateType.uninitialized: colors = BrandColors.uninitializedGradientColors; break; case StateType.stable: - colors = BrandColors.stableGradientColors; + colors = [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.tertiary, + ]; break; case StateType.warning: colors = BrandColors.warningGradientColors; break; } return ShaderMask( - shaderCallback: (bounds) => LinearGradient( - begin: Alignment(-1, -0.8), - end: Alignment(0.9, 0.9), + shaderCallback: (final bounds) => LinearGradient( + begin: const Alignment(-1, -0.8), + end: const Alignment(0.9, 0.9), colors: colors, tileMode: TileMode.mirror, ).createShader(bounds), diff --git a/lib/ui/components/jobs_content/jobs_content.dart b/lib/ui/components/jobs_content/jobs_content.dart index 92298a06..bd8166de 100644 --- a/lib/ui/components/jobs_content/jobs_content.dart +++ b/lib/ui/components/jobs_content/jobs_content.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; @@ -13,97 +13,113 @@ import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class JobsContent extends StatelessWidget { - const JobsContent({Key? key}) : super(key: key); + const JobsContent({final super.key}); @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - late final List widgets; - if (state is JobsStateEmpty) { - widgets = [ - SizedBox(height: 80), - Center(child: BrandText.body1('jobs.empty'.tr())), - SizedBox(height: 80), - BrandButton.rised( - onPressed: () => context.read().upgradeServer(), - text: 'jobs.upgradeServer'.tr(), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () { - var nav = getIt(); - nav.showPopUpDialog(BrandAlert( - title: 'jobs.rebootServer'.tr(), - contentText: 'modals.3'.tr(), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => - {context.read().rebootServer()}, - text: 'modals.9'.tr(), - ) - ], - )); - }, - title: 'jobs.rebootServer'.tr(), - ), - ]; - } else if (state is JobsStateLoading) { - widgets = [ - SizedBox(height: 80), - BrandLoader.horizontal(), - ]; - } else if (state is JobsStateWithJobs) { - widgets = [ - ...state.jobList - .map( - (j) => Row( - children: [ - Expanded( - child: BrandCards.small( - child: Text(j.title), - ), + Widget build(final BuildContext context) => BlocBuilder( + builder: (final context, final state) { + late List widgets; + final ServerInstallationState installationState = + context.read().state; + if (state is JobsStateEmpty) { + widgets = [ + const SizedBox(height: 80), + Center(child: BrandText.body1('jobs.empty'.tr())), + ]; + + if (installationState is ServerInstallationFinished) { + widgets = [ + ...widgets, + const SizedBox(height: 80), + BrandButton.rised( + onPressed: () => context.read().upgradeServer(), + text: 'jobs.upgradeServer'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () { + final NavigationService nav = getIt(); + nav.showPopUpDialog( + BrandAlert( + title: 'jobs.rebootServer'.tr(), + contentText: 'modals.3'.tr(), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), + ), + ActionButton( + onPressed: () => + {context.read().rebootServer()}, + text: 'modals.9'.tr(), + ) + ], ), - SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - primary: BrandColors.red1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + ); + }, + title: 'jobs.rebootServer'.tr(), + ), + ]; + } + } else if (state is JobsStateLoading) { + widgets = [ + const SizedBox(height: 80), + BrandLoader.horizontal(), + ]; + } else if (state is JobsStateWithJobs) { + widgets = [ + ...state.jobList + .map( + (final j) => Row( + children: [ + Expanded( + child: BrandCards.small( + child: Text(j.title), ), ), - onPressed: () => - context.read().removeJob(j.id), - child: Text('basis.remove'.tr()), - ), - ], - ), - ) - .toList(), - SizedBox(height: 20), - BrandButton.rised( - onPressed: () => context.read().applyAll(), - text: 'jobs.start'.tr(), - ), - ]; - } - return ListView( - padding: paddingH15V0, - children: [ - SizedBox(height: 15), - Center( - child: BrandText.h2( - 'jobs.title'.tr(), + const SizedBox(width: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: + Theme.of(context).colorScheme.errorContainer, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () => + context.read().removeJob(j.id), + child: Text( + 'basis.remove'.tr(), + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onErrorContainer, + ), + ), + ), + ], + ), + ) + .toList(), + const SizedBox(height: 20), + BrandButton.rised( + onPressed: () => context.read().applyAll(), + text: 'jobs.start'.tr(), ), - ), - SizedBox(height: 20), - ...widgets - ], - ); - }, - ); - } + ]; + } + return ListView( + padding: paddingH15V0, + children: [ + const SizedBox(height: 15), + Center( + child: BrandText.h2( + 'jobs.title'.tr(), + ), + ), + const SizedBox(height: 20), + ...widgets + ], + ); + }, + ); } diff --git a/lib/ui/components/not_ready_card/not_ready_card.dart b/lib/ui/components/not_ready_card/not_ready_card.dart index 7d1c6cc5..49947c1b 100644 --- a/lib/ui/components/not_ready_card/not_ready_card.dart +++ b/lib/ui/components/not_ready_card/not_ready_card.dart @@ -1,54 +1,54 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; class NotReadyCard extends StatelessWidget { - const NotReadyCard({Key? key}) : super(key: key); + const NotReadyCard({final super.key}); @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), color: BrandColors.gray6), - child: RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'not_ready_card.1'.tr(), - style: TextStyle(color: BrandColors.white), - ), - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 0.5), - child: GestureDetector( - onTap: () => Navigator.of(context).push( - materialRoute( - InitializingPage(), + Widget build(final BuildContext context) => Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: BrandColors.gray6, + ), + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'not_ready_card.1'.tr(), + style: const TextStyle(color: BrandColors.white), + ), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.5), + child: GestureDetector( + onTap: () => Navigator.of(context).push( + materialRoute( + const InitializingPage(), + ), ), - ), - child: Text( - 'not_ready_card.2'.tr(), - style: body1Style.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - decoration: TextDecoration.underline, - // height: 1.1, + child: Text( + 'not_ready_card.2'.tr(), + style: body1Style.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + // height: 1.1, + ), ), ), ), ), - ), - TextSpan( - text: 'not_ready_card.3'.tr(), - style: TextStyle(color: BrandColors.white), - ), - ], + TextSpan( + text: 'not_ready_card.3'.tr(), + style: const TextStyle(color: BrandColors.white), + ), + ], + ), ), - ), - ); - } + ); } diff --git a/lib/ui/components/one_page/one_page.dart b/lib/ui/components/one_page/one_page.dart index 9cb1afe6..d16dd5f3 100644 --- a/lib/ui/components/one_page/one_page.dart +++ b/lib/ui/components/one_page/one_page.dart @@ -6,44 +6,43 @@ import 'package:selfprivacy/ui/components/pre_styled_buttons/pre_styled_buttons. class OnePage extends StatelessWidget { const OnePage({ - Key? key, required this.title, required this.child, - }) : super(key: key); + final super.key, + }); final String title; final Widget child; @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - child: Column( - children: [ - Container( - height: 51, - alignment: Alignment.center, - padding: EdgeInsets.symmetric(horizontal: 15), - child: BrandText.h4('basis.details'.tr()), - ), - BrandDivider(), - ], - ), - preferredSize: Size.fromHeight(52), - ), - body: child, - bottomNavigationBar: SafeArea( - child: Container( - decoration: BoxDecoration(boxShadow: kElevationToShadow[3]), - height: kBottomNavigationBarHeight, - child: Container( - color: Theme.of(context).scaffoldBackgroundColor, - alignment: Alignment.center, - child: PreStyledButtons.close( - onPress: () => Navigator.of(context).pop()), + Widget build(final BuildContext context) => Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: Column( + children: [ + Container( + height: 51, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 15), + child: BrandText.h4('basis.details'.tr()), + ), + const BrandDivider(), + ], ), ), - ), - ); - } + body: child, + bottomNavigationBar: SafeArea( + child: Container( + decoration: BoxDecoration(boxShadow: kElevationToShadow[3]), + height: kBottomNavigationBarHeight, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + alignment: Alignment.center, + child: PreStyledButtons.close( + onPress: () => Navigator.of(context).pop(), + ), + ), + ), + ), + ); } diff --git a/lib/ui/components/pre_styled_buttons/close.dart b/lib/ui/components/pre_styled_buttons/close.dart index 13f99bce..48a1bddb 100644 --- a/lib/ui/components/pre_styled_buttons/close.dart +++ b/lib/ui/components/pre_styled_buttons/close.dart @@ -1,21 +1,19 @@ part of 'pre_styled_buttons.dart'; class _CloseButton extends StatelessWidget { - const _CloseButton({Key? key, required this.onPress}) : super(key: key); + const _CloseButton({required this.onPress}); final VoidCallback onPress; @override - Widget build(BuildContext context) { - return OutlinedButton( - onPressed: () => Navigator.of(context).pop(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - BrandText.h4('basis.close'.tr()), - Icon(Icons.close), - ], - ), - ); - } + Widget build(final BuildContext context) => OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + BrandText.h4('basis.close'.tr()), + const Icon(Icons.close), + ], + ), + ); } diff --git a/lib/ui/components/pre_styled_buttons/flash.dart b/lib/ui/components/pre_styled_buttons/flash.dart index 5e9b1875..3e780fd7 100644 --- a/lib/ui/components/pre_styled_buttons/flash.dart +++ b/lib/ui/components/pre_styled_buttons/flash.dart @@ -1,8 +1,6 @@ part of 'pre_styled_buttons.dart'; class _BrandFlashButton extends StatefulWidget { - _BrandFlashButton({Key? key}) : super(key: key); - @override _BrandFlashButtonState createState() => _BrandFlashButtonState(); } @@ -14,18 +12,20 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> @override void initState() { - _animationController = - AnimationController(vsync: this, duration: Duration(milliseconds: 800)); + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); _colorTween = ColorTween( begin: BrandColors.black, end: BrandColors.primary, ).animate(_animationController); super.initState(); - WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); } - void _afterLayout(_) { + void _afterLayout(final _) { if (Theme.of(context).brightness == Brightness.dark) { setState(() { _colorTween = ColorTween( @@ -45,33 +45,34 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> bool wasPrevStateIsEmpty = true; @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { - wasPrevStateIsEmpty = false; - _animationController.forward(); - } else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) { - wasPrevStateIsEmpty = true; + Widget build(final BuildContext context) => + BlocListener( + listener: (final context, final state) { + if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { + wasPrevStateIsEmpty = false; + _animationController.forward(); + } else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) { + wasPrevStateIsEmpty = true; - _animationController.reverse(); - } - }, - child: IconButton( - onPressed: () { - showBrandBottomSheet( - context: context, - builder: (context) => BrandBottomSheet( - isExpended: true, - child: JobsContent(), - ), - ); + _animationController.reverse(); + } }, - icon: AnimatedBuilder( + child: IconButton( + onPressed: () { + showBrandBottomSheet( + context: context, + builder: (final context) => const BrandBottomSheet( + isExpended: true, + child: JobsContent(), + ), + ); + }, + icon: AnimatedBuilder( animation: _colorTween, - builder: (context, child) { - var v = _animationController.value; - var icon = v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; + builder: (final context, final child) { + final double v = _animationController.value; + final IconData icon = + v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; return Transform.scale( scale: 1 + (v < 0.5 ? v : 1 - v) * 2, child: Icon( @@ -79,8 +80,8 @@ class _BrandFlashButtonState extends State<_BrandFlashButton> color: _colorTween.value, ), ); - }), - ), - ); - } + }, + ), + ), + ); } diff --git a/lib/ui/components/pre_styled_buttons/flash_fab.dart b/lib/ui/components/pre_styled_buttons/flash_fab.dart new file mode 100644 index 00000000..4ae29087 --- /dev/null +++ b/lib/ui/components/pre_styled_buttons/flash_fab.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ionicons/ionicons.dart'; +import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/jobs_content/jobs_content.dart'; +import 'package:selfprivacy/ui/helpers/modals.dart'; + +class BrandFab extends StatefulWidget { + const BrandFab({final super.key}); + + @override + State createState() => _BrandFabState(); +} + +class _BrandFabState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _colorTween; + + @override + void initState() { + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + super.initState(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + bool wasPrevStateIsEmpty = true; + + @override + Widget build(final BuildContext context) { + _colorTween = ColorTween( + begin: Theme.of(context).colorScheme.onPrimaryContainer, + end: Theme.of(context).colorScheme.primary, + ).animate(_animationController); + + return BlocListener( + listener: (final BuildContext context, final JobsState state) { + if (wasPrevStateIsEmpty && state is! JobsStateEmpty) { + wasPrevStateIsEmpty = false; + _animationController.forward(); + } else if (!wasPrevStateIsEmpty && state is JobsStateEmpty) { + wasPrevStateIsEmpty = true; + + _animationController.reverse(); + } + }, + child: FloatingActionButton( + onPressed: () { + showBrandBottomSheet( + context: context, + builder: (final BuildContext context) => const BrandBottomSheet( + isExpended: true, + child: JobsContent(), + ), + ); + }, + child: AnimatedBuilder( + animation: _colorTween, + builder: (final BuildContext context, final Widget? child) { + final double v = _animationController.value; + final IconData icon = + v > 0.5 ? Ionicons.flash : Ionicons.flash_outline; + return Transform.scale( + scale: 1 + (v < 0.5 ? v : 1 - v) * 2, + child: Icon( + icon, + color: _colorTween.value, + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart index b3be1ee8..ad9105fb 100644 --- a/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart +++ b/lib/ui/components/pre_styled_buttons/pre_styled_buttons.dart @@ -14,7 +14,7 @@ part 'flash.dart'; class PreStyledButtons { static Widget close({ - required VoidCallback onPress, + required final VoidCallback onPress, }) => _CloseButton(onPress: onPress); diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 41b23c86..4de729f7 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -6,39 +6,41 @@ import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; class ProgressBar extends StatefulWidget { - ProgressBar({ - Key? key, + const ProgressBar({ required this.steps, required this.activeIndex, - }) : super(key: key); + final super.key, + }); final int activeIndex; final List steps; @override - _ProgressBarState createState() => _ProgressBarState(); + State createState() => _ProgressBarState(); } class _ProgressBarState extends State { @override - Widget build(BuildContext context) { - double progress = 1 / widget.steps.length * (widget.activeIndex + 0.3); - var isDark = context.watch().state.isDarkModeOn; - var style = isDark ? progressTextStyleDark : progressTextStyleLight; + Widget build(final BuildContext context) { + final double progress = + 1 / widget.steps.length * (widget.activeIndex + 0.3); + final bool isDark = context.watch().state.isDarkModeOn; + final TextStyle style = + isDark ? progressTextStyleDark : progressTextStyleLight; - var allSteps = widget.steps.asMap().map( - (i, step) { - var value = _stepTitle(index: i, style: style, step: step); + final Iterable allSteps = widget.steps.asMap().map( + (final i, final step) { + final Container value = _stepTitle(index: i, style: style, step: step); return MapEntry(i, value); }, ).values; - List odd = []; - List even = []; + final List odd = []; + final List even = []; - var i = 0; - for (var step in allSteps) { + int i = 0; + for (final Container step in allSteps) { if (i.isEven) { even.add(step); } else { @@ -49,12 +51,12 @@ class _ProgressBarState extends State { odd.insert( 0, - SizedBox( + const SizedBox( width: 10, ), ); odd.add( - SizedBox( + const SizedBox( width: 20, ), ); @@ -63,12 +65,12 @@ class _ProgressBarState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ BrandText.h2('Progress'), - SizedBox(height: 10), + const SizedBox(height: 10), Row( - children: even, mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: even, ), - SizedBox(height: 7), + const SizedBox(height: 7), Container( alignment: Alignment.centerLeft, decoration: BoxDecoration( @@ -76,45 +78,43 @@ class _ProgressBarState extends State { borderRadius: BorderRadius.circular(5), ), child: LayoutBuilder( - builder: (_, constraints) { - return AnimatedContainer( - width: constraints.maxWidth * progress, - height: 5, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: BrandColors.stableGradientColors, - ), + builder: (final _, final constraints) => AnimatedContainer( + width: constraints.maxWidth * progress, + height: 5, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: BrandColors.stableGradientColors, ), - duration: Duration( - milliseconds: 300, - ), - ); - }, + ), + duration: const Duration( + milliseconds: 300, + ), + ), ), ), - SizedBox(height: 5), + const SizedBox(height: 5), Row( - children: odd, mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: odd, ), ], ); } Container _stepTitle({ - required int index, + required final int index, TextStyle? style, - String? step, + final String? step, }) { - var isActive = index == widget.activeIndex; - var checked = index < widget.activeIndex; + final bool isActive = index == widget.activeIndex; + final bool checked = index < widget.activeIndex; style = isActive ? style!.copyWith(fontWeight: FontWeight.w700) : style; return Container( - padding: EdgeInsets.only(left: 10), + padding: const EdgeInsets.only(left: 10), height: 20, alignment: Alignment.center, child: RichText( @@ -122,13 +122,15 @@ class _ProgressBarState extends State { text: TextSpan( style: progressTextStyleLight, children: [ - checked - ? WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(bottom: 2, right: 2), - child: Icon(BrandIcons.check, size: 11), - )) - : TextSpan(text: '${index + 1}.', style: style), + if (checked) + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(bottom: 2, right: 2), + child: Icon(BrandIcons.check, size: 11), + ), + ) + else + TextSpan(text: '${index + 1}.', style: style), TextSpan(text: step, style: style) ], ), diff --git a/lib/ui/components/switch_block/switch_bloc.dart b/lib/ui/components/switch_block/switch_bloc.dart index cddb2859..ae593f1e 100644 --- a/lib/ui/components/switch_block/switch_bloc.dart +++ b/lib/ui/components/switch_block/switch_bloc.dart @@ -3,38 +3,37 @@ import 'package:selfprivacy/config/brand_colors.dart'; class SwitcherBlock extends StatelessWidget { const SwitcherBlock({ - Key? key, required this.child, required this.isActive, required this.onChange, - }) : super(key: key); + final super.key, + }); final Widget child; final bool isActive; final ValueChanged onChange; @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + Widget build(final BuildContext context) => Container( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible(child: child), - SizedBox(width: 5), - Switch( - activeColor: BrandColors.green1, - activeTrackColor: BrandColors.green2, - onChanged: onChange, - value: isActive, + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), ), - ], - ), - ); - } + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible(child: child), + const SizedBox(width: 5), + Switch( + activeColor: BrandColors.green1, + activeTrackColor: BrandColors.green2, + onChanged: onChange, + value: isActive, + ), + ], + ), + ); } diff --git a/lib/ui/helpers/modals.dart b/lib/ui/helpers/modals.dart index 69f6b6d8..8867885f 100644 --- a/lib/ui/helpers/modals.dart +++ b/lib/ui/helpers/modals.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; Future showBrandBottomSheet({ - required BuildContext context, - required WidgetBuilder builder, + required final BuildContext context, + required final WidgetBuilder builder, }) => showCupertinoModalBottomSheet( builder: builder, barrierColor: Colors.black45, context: context, - shadow: BoxShadow(color: Colors.transparent), + shadow: const BoxShadow(color: Colors.transparent), backgroundColor: Colors.transparent, ); diff --git a/lib/ui/pages/backup_details/backup_details.dart b/lib/ui/pages/backup_details/backup_details.dart index 3040d1aa..55a8dd12 100644 --- a/lib/ui/pages/backup_details/backup_details.dart +++ b/lib/ui/pages/backup_details/backup_details.dart @@ -2,9 +2,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; -import 'package:selfprivacy/logic/models/backup.dart'; +import 'package:selfprivacy/logic/models/json/backup.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; @@ -13,34 +13,38 @@ import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.da import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import '../../components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; -var navigatorKey = GlobalKey(); +GlobalKey navigatorKey = GlobalKey(); class BackupDetails extends StatefulWidget { - const BackupDetails({Key? key}) : super(key: key); + const BackupDetails({final super.key}); @override - _BackupDetailsState createState() => _BackupDetailsState(); + State createState() => _BackupDetailsState(); } class _BackupDetailsState extends State with SingleTickerProviderStateMixin { @override - Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; - var isBackupInitialized = context.watch().state.isInitialized; - var backupStatus = context.watch().state.status; - var providerState = isReady && isBackupInitialized + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + final bool isBackupInitialized = + context.watch().state.isInitialized; + final BackupStatusEnum backupStatus = + context.watch().state.status; + final StateType providerState = isReady && isBackupInitialized ? (backupStatus == BackupStatusEnum.error ? StateType.warning : StateType.stable) : StateType.uninitialized; - var preventActions = context.watch().state.preventActions; - var backupProgress = context.watch().state.progress; - var backupError = context.watch().state.error; - var backups = context.watch().state.backups; - var refreshing = context.watch().state.refreshing; + final bool preventActions = + context.watch().state.preventActions; + final double backupProgress = context.watch().state.progress; + final String backupError = context.watch().state.error; + final List backups = context.watch().state.backups; + final bool refreshing = context.watch().state.refreshing; return BrandHeroScreen( heroIcon: BrandIcons.save, @@ -71,7 +75,7 @@ class _BackupDetailsState extends State : () async { await context.read().createBackup(); }, - leading: Icon( + leading: const Icon( Icons.add_circle_outline_rounded, ), title: Text( @@ -83,7 +87,8 @@ class _BackupDetailsState extends State ListTile( title: Text( 'providers.backup.creating'.tr( - args: [(backupProgress * 100).round().toString()]), + args: [(backupProgress * 100).round().toString()], + ), style: Theme.of(context).textTheme.headline6, ), subtitle: LinearProgressIndicator( @@ -95,7 +100,8 @@ class _BackupDetailsState extends State ListTile( title: Text( 'providers.backup.restoring'.tr( - args: [(backupProgress * 100).round().toString()]), + args: [(backupProgress * 100).round().toString()], + ), style: Theme.of(context).textTheme.headline6, ), subtitle: LinearProgressIndicator( @@ -104,7 +110,7 @@ class _BackupDetailsState extends State ), if (backupStatus == BackupStatusEnum.error) ListTile( - leading: Icon( + leading: const Icon( Icons.error_outline, color: BrandColors.red1, ), @@ -116,7 +122,7 @@ class _BackupDetailsState extends State ], ), ), - SizedBox(height: 16), + const SizedBox(height: 16), // Card with a list of existing backups // Each list item has a date // When clicked, starts the restore action @@ -127,7 +133,7 @@ class _BackupDetailsState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - leading: Icon( + leading: const Icon( Icons.refresh, ), title: Text( @@ -135,57 +141,61 @@ class _BackupDetailsState extends State style: Theme.of(context).textTheme.headline6, ), ), - Divider( + const Divider( height: 1.0, ), if (backups.isEmpty) ListTile( - leading: Icon( + leading: const Icon( Icons.error_outline, ), title: Text('providers.backup.no_backups'.tr()), ), if (backups.isNotEmpty) Column( - children: backups.map((backup) { - return ListTile( - onTap: preventActions - ? null - : () { - var nav = getIt(); - nav.showPopUpDialog(BrandAlert( - title: 'providers.backup.restoring'.tr(), - contentText: 'providers.backup.restore_alert' - .tr(args: [backup.time.toString()]), - actions: [ - ActionButton( - text: 'basis.cancel'.tr(), - ), - ActionButton( - onPressed: () => { - context - .read() - .restoreBackup(backup.id) - }, - text: 'modals.yes'.tr(), - ) - ], - )); - }, - title: Text( - MaterialLocalizations.of(context) - .formatShortDate(backup.time) + - ' ' + - TimeOfDay.fromDateTime(backup.time) - .format(context), - ), - ); - }).toList(), + children: backups + .map( + (final Backup backup) => ListTile( + onTap: preventActions + ? null + : () { + final NavigationService nav = + getIt(); + nav.showPopUpDialog( + BrandAlert( + title: + 'providers.backup.restoring'.tr(), + contentText: + 'providers.backup.restore_alert'.tr( + args: [backup.time.toString()], + ), + actions: [ + ActionButton( + text: 'basis.cancel'.tr(), + ), + ActionButton( + onPressed: () => { + context + .read() + .restoreBackup(backup.id) + }, + text: 'modals.yes'.tr(), + ) + ], + ), + ); + }, + title: Text( + '${MaterialLocalizations.of(context).formatShortDate(backup.time)} ${TimeOfDay.fromDateTime(backup.time).format(context)}', + ), + ), + ) + .toList(), ), ], ), ), - SizedBox(height: 16), + const SizedBox(height: 16), BrandCards.outlined( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -202,7 +212,7 @@ class _BackupDetailsState extends State if (providerState != StateType.uninitialized) Column( children: [ - Divider( + const Divider( height: 1.0, ), ListTile( @@ -217,7 +227,7 @@ class _BackupDetailsState extends State .forceUpdateBackups() }, ), - Divider( + const Divider( height: 1.0, ), ListTile( diff --git a/lib/ui/pages/devices/devices.dart b/lib/ui/pages/devices/devices.dart new file mode 100644 index 00000000..ad48096e --- /dev/null +++ b/lib/ui/pages/devices/devices.dart @@ -0,0 +1,174 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/models/json/api_token.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/devices/new_device.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class DevicesScreen extends StatefulWidget { + const DevicesScreen({final super.key}); + + @override + State createState() => _DevicesScreenState(); +} + +class _DevicesScreenState extends State { + @override + Widget build(final BuildContext context) { + final ApiDevicesState devicesStatus = + context.watch().state; + + return RefreshIndicator( + onRefresh: () async { + context.read().refresh(); + }, + child: BrandHeroScreen( + heroTitle: 'devices.main_screen.header'.tr(), + heroSubtitle: 'devices.main_screen.description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + if (devicesStatus.status == LoadingStatus.uninitialized) ...[ + const Center( + heightFactor: 8, + child: CircularProgressIndicator(), + ), + ], + if (devicesStatus.status != LoadingStatus.uninitialized) ...[ + _DevicesInfo( + devicesStatus: devicesStatus, + ), + const SizedBox(height: 16), + OutlinedButton( + onPressed: () => Navigator.of(context) + .push(materialRoute(const NewDeviceScreen())), + child: Text('devices.main_screen.authorize_new_device'.tr()), + ), + const SizedBox(height: 16), + const Divider(height: 1), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + 'devices.main_screen.tip'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ], + const SizedBox(height: 24), + ], + ), + ); + } +} + +class _DevicesInfo extends StatelessWidget { + const _DevicesInfo({ + required this.devicesStatus, + }); + + final ApiDevicesState devicesStatus; + + @override + Widget build(final BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'devices.main_screen.this_device'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + _DeviceTile(device: devicesStatus.thisDevice), + const Divider(height: 1), + const SizedBox(height: 16), + Text( + 'devices.main_screen.other_devices'.tr(), + style: Theme.of(context).textTheme.labelLarge!.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ...devicesStatus.otherDevices + .map((final device) => _DeviceTile(device: device)) + .toList(), + ], + ); +} + +class _DeviceTile extends StatelessWidget { + const _DeviceTile({required this.device}); + + final ApiToken device; + + @override + Widget build(final BuildContext context) => ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + title: Text(device.name), + subtitle: Text( + 'devices.main_screen.access_granted_on' + .tr(args: [DateFormat.yMMMMd().format(device.date)]), + ), + onTap: device.isCaller + ? null + : () => _showConfirmationDialog(context, device), + ); + + Future _showConfirmationDialog( + final BuildContext context, + final ApiToken device, + ) => + showDialog( + context: context, + builder: (final context) => AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.link_off_outlined), + const SizedBox(height: 16), + Text( + 'devices.revoke_device_alert.header'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'devices.revoke_device_alert.description' + .tr(args: [device.name]), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + actions: [ + TextButton( + child: Text('devices.revoke_device_alert.no'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('devices.revoke_device_alert.yes'.tr()), + onPressed: () { + context.read().deleteDevice(device); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); +} diff --git a/lib/ui/pages/devices/new_device.dart b/lib/ui/pages/devices/new_device.dart new file mode 100644 index 00000000..e8173db0 --- /dev/null +++ b/lib/ui/pages/devices/new_device.dart @@ -0,0 +1,83 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/devices/devices_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class NewDeviceScreen extends StatelessWidget { + const NewDeviceScreen({final super.key}); + + @override + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'devices.add_new_device_screen.header'.tr(), + heroSubtitle: 'devices.add_new_device_screen.description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder( + future: context.read().getNewDeviceKey(), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + return _KeyDisplay( + newDeviceKey: snapshot.data.toString(), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), + ], + ); +} + +class _KeyDisplay extends StatelessWidget { + const _KeyDisplay({required this.newDeviceKey}); + final String newDeviceKey; + + @override + Widget build(final BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(), + const SizedBox(height: 16), + Text( + newDeviceKey, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 24, + fontFamily: 'RobotoMono', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.onBackground, + ), + const SizedBox(height: 16), + Text( + 'devices.add_new_device_screen.tip'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + const SizedBox(height: 16), + FilledButton( + child: Text( + 'basis.done'.tr(), + ), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(height: 24), + ], + ); +} diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index 8973e963..44d6db0d 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -1,41 +1,46 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/get_it_config.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; class DnsDetailsPage extends StatefulWidget { + const DnsDetailsPage({final super.key}); + @override - _DnsDetailsPageState createState() => _DnsDetailsPageState(); + State createState() => _DnsDetailsPageState(); } class _DnsDetailsPageState extends State { - Widget _getStateCard(DnsRecordsStatus dnsState, Function fixCallback) { - var description = ''; - var subtitle = ''; - var icon = Icon( + Widget _getStateCard( + final DnsRecordsStatus dnsState, + final Function fixCallback, + ) { + String description = ''; + String subtitle = ''; + Icon icon = const Icon( Icons.check, color: Colors.green, ); switch (dnsState) { case DnsRecordsStatus.uninitialized: description = 'providers.domain.states.uninitialized'.tr(); - icon = Icon( + icon = const Icon( Icons.refresh, ); break; case DnsRecordsStatus.refreshing: description = 'providers.domain.states.refreshing'.tr(); - icon = Icon( + icon = const Icon( Icons.refresh, ); break; case DnsRecordsStatus.good: description = 'providers.domain.states.ok'.tr(); - icon = Icon( + icon = const Icon( Icons.check, color: Colors.green, ); @@ -43,7 +48,7 @@ class _DnsDetailsPageState extends State { case DnsRecordsStatus.error: description = 'providers.domain.states.error'.tr(); subtitle = 'providers.domain.states.error_subtitle'.tr(); - icon = Icon( + icon = const Icon( Icons.error, color: Colors.red, ); @@ -61,10 +66,12 @@ class _DnsDetailsPageState extends State { } @override - Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; - final domain = getIt().cloudFlareDomain?.domainName ?? ''; - var dnsCubit = context.watch().state; + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + final String domain = + getIt().serverDomain?.domainName ?? ''; + final DnsRecordsState dnsCubit = context.watch().state; print(dnsCubit.dnsState); @@ -103,7 +110,7 @@ class _DnsDetailsPageState extends State { ), ), - SizedBox(height: 16.0), + const SizedBox(height: 16.0), // Outlined card with a list of A records and their // status. BrandCards.outlined( @@ -121,13 +128,13 @@ class _DnsDetailsPageState extends State { ), ...dnsCubit.dnsRecords .where( - (dnsRecord) => + (final dnsRecord) => dnsRecord.category == DnsRecordsCategory.services, ) .map( - (dnsRecord) => Column( + (final dnsRecord) => Column( children: [ - Divider( + const Divider( height: 1.0, ), ListTile( @@ -161,7 +168,7 @@ class _DnsDetailsPageState extends State { ], ), ), - SizedBox(height: 16.0), + const SizedBox(height: 16.0), BrandCards.outlined( child: Column( children: [ @@ -177,13 +184,13 @@ class _DnsDetailsPageState extends State { ), ...dnsCubit.dnsRecords .where( - (dnsRecord) => + (final dnsRecord) => dnsRecord.category == DnsRecordsCategory.email, ) .map( - (dnsRecord) => Column( + (final dnsRecord) => Column( children: [ - Divider( + const Divider( height: 1.0, ), ListTile( diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/initializing/initializing.dart deleted file mode 100644 index d30569ca..00000000 --- a/lib/ui/pages/initializing/initializing.dart +++ /dev/null @@ -1,530 +0,0 @@ -import 'package:cubit_form/cubit_form.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/backblaze_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/cloudflare_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/domain_cloudflare.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/hetzner_form_cubit.dart'; -import 'package:selfprivacy/logic/cubit/forms/initializing/root_user_form_cubit.dart'; -import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; -import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; -import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; -import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; -import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; - -class InitializingPage extends StatelessWidget { - @override - Widget build(BuildContext context) { - var cubit = context.watch(); - var actualPage = [ - () => _stepHetzner(cubit), - () => _stepCloudflare(cubit), - () => _stepBackblaze(cubit), - () => _stepDomain(cubit), - () => _stepUser(cubit), - () => _stepServer(cubit), - () => _stepCheck(cubit), - () => _stepCheck(cubit), - () => _stepCheck(cubit), - () => Container(child: Center(child: Text('initializing.finish'.tr()))) - ][cubit.state.progress](); - return BlocListener( - listener: (context, state) { - if (cubit.state is AppConfigFinished) { - Navigator.of(context).pushReplacement(materialRoute(RootPage())); - } - }, - child: SafeArea( - child: Scaffold( - body: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: paddingH15V0.copyWith(top: 10, bottom: 10), - child: cubit.state.isFullyInitilized - ? SizedBox( - height: 80, - ) - : ProgressBar( - steps: [ - 'Hetzner', - 'CloudFlare', - 'Backblaze', - 'Domain', - 'User', - 'Server', - '✅ Check', - ], - activeIndex: cubit.state.porgressBar, - ), - ), - _addCard( - AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: actualPage, - ), - ), - ConstrainedBox( - constraints: BoxConstraints( - minHeight: MediaQuery.of(context).size.height - - MediaQuery.of(context).padding.top - - MediaQuery.of(context).padding.bottom - - 566, - ), - child: Container( - alignment: Alignment.center, - child: BrandButton.text( - title: cubit.state is AppConfigFinished - ? 'basis.close'.tr() - : 'basis.later'.tr(), - onPressed: () { - Navigator.of(context).pushAndRemoveUntil( - materialRoute(RootPage()), - (predicate) => false, - ); - }, - ), - )), - ], - ), - ), - ), - ), - ); - } - - Widget _stepHetzner(AppConfigCubit initializingCubit) { - return BlocProvider( - create: (context) => HetznerFormCubit(initializingCubit), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/hetzner.png', - width: 150, - ), - SizedBox(height: 10), - BrandText.h2('initializing.1'.tr()), - SizedBox(height: 10), - BrandText.body2('initializing.2'.tr()), - Spacer(), - CubitFormTextField( - formFieldCubit: context.read().apiKey, - textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'Hetzner API Token', - ), - ), - Spacer(), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => - _showModal(context, _HowTo(fileName: 'how_hetzner')), - title: 'initializing.how'.tr(), - ), - ], - ); - }), - ); - } - - void _showModal(BuildContext context, Widget widget) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return widget; - }, - ); - } - - Widget _stepCloudflare(AppConfigCubit initializingCubit) { - return BlocProvider( - create: (context) => CloudFlareFormCubit(initializingCubit), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/cloudflare.png', - width: 150, - ), - SizedBox(height: 10), - BrandText.h2('initializing.3'.tr()), - SizedBox(height: 10), - BrandText.body2('initializing.4'.tr()), - Spacer(), - CubitFormTextField( - formFieldCubit: context.read().apiKey, - textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'initializing.5'.tr(), - ), - ), - Spacer(), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal( - context, - _HowTo( - fileName: 'how_cloudflare', - )), - title: 'initializing.how'.tr(), - ), - ], - ); - }), - ); - } - - Widget _stepBackblaze(AppConfigCubit initializingCubit) { - return BlocProvider( - create: (context) => BackblazeFormCubit(initializingCubit), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/backblaze.png', - height: 50, - ), - SizedBox(height: 10), - BrandText.h2('initializing.6'.tr()), - SizedBox(height: 10), - Spacer(), - CubitFormTextField( - formFieldCubit: context.read().keyId, - textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'KeyID', - ), - ), - Spacer(), - CubitFormTextField( - formFieldCubit: context.read().applicationKey, - textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'Master Application Key', - ), - ), - Spacer(), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - SizedBox(height: 10), - BrandButton.text( - onPressed: () => _showModal( - context, - _HowTo( - fileName: 'how_backblaze', - )), - title: 'initializing.how'.tr(), - ), - ], - ); - }), - ); - } - - Widget _stepDomain(AppConfigCubit initializingCubit) { - return BlocProvider( - create: (context) => DomainSetupCubit(initializingCubit)..load(), - child: Builder(builder: (context) { - DomainSetupState state = context.watch().state; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Image.asset( - 'assets/images/logos/cloudflare.png', - width: 150, - ), - SizedBox(height: 30), - BrandText.h2('basis.domain'.tr()), - SizedBox(height: 10), - if (state is Empty) BrandText.body2('initializing.7'.tr()), - if (state is Loading) - BrandText.body2( - state.type == LoadingTypes.loadingDomain - ? 'initializing.8'.tr() - : 'basis.saving'.tr(), - ), - if (state is MoreThenOne) - BrandText.body2( - 'initializing.9'.tr(), - ), - if (state is Loaded) ...[ - SizedBox(height: 10), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: BrandText.h3( - '${state.domain}', - textAlign: TextAlign.center, - ), - ), - Container( - width: 50, - child: BrandButton.rised( - onPressed: () => context.read().load(), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.refresh, - color: Colors.white, - ), - ], - ), - ), - ), - ], - ) - ], - if (state is Empty) ...[ - SizedBox(height: 30), - BrandButton.rised( - onPressed: () => context.read().load(), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.refresh, - color: Colors.white, - ), - SizedBox(width: 10), - BrandText.buttonTitleText('Обновить cписок'), - ], - ), - ), - ], - if (state is Loaded) ...[ - SizedBox(height: 30), - BrandButton.rised( - onPressed: () => context.read().saveDomain(), - text: 'initializing.10'.tr(), - ), - ], - SizedBox( - height: 10, - width: double.infinity, - ), - ], - ); - }), - ); - } - - Widget _stepUser(AppConfigCubit initializingCubit) { - return BlocProvider( - create: (context) => - RootUserFormCubit(initializingCubit, FieldCubitFactory(context)), - child: Builder(builder: (context) { - var formCubitState = context.watch().state; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.h2('initializing.22'.tr()), - SizedBox(height: 10), - BrandText.body2('initializing.23'.tr()), - Spacer(), - CubitFormTextField( - formFieldCubit: context.read().userName, - textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'basis.nickname'.tr(), - ), - ), - SizedBox(height: 10), - BlocBuilder, FieldCubitState>( - bloc: context.read().isVisible, - builder: (context, state) { - var isVisible = state.value; - return CubitFormTextField( - obscureText: !isVisible, - formFieldCubit: context.read().password, - textAlign: TextAlign.center, - scrollPadding: EdgeInsets.only(bottom: 70), - decoration: InputDecoration( - hintText: 'basis.password'.tr(), - suffixIcon: IconButton( - icon: Icon( - isVisible ? Icons.visibility : Icons.visibility_off, - ), - onPressed: () => context - .read() - .isVisible - .setValue(!isVisible), - ), - suffixIconConstraints: BoxConstraints(minWidth: 60), - prefixIconConstraints: BoxConstraints(maxWidth: 85), - prefixIcon: Container(), - ), - ); - }, - ), - Spacer(), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.connect'.tr(), - ), - ], - ); - }), - ); - } - - Widget _stepServer(AppConfigCubit appConfigCubit) { - var isLoading = (appConfigCubit.state as AppConfigNotFinished).isLoading; - return Builder(builder: (context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Spacer(flex: 2), - BrandText.h2('initializing.final'.tr()), - SizedBox(height: 10), - BrandText.body2('initializing.11'.tr()), - Spacer(), - BrandButton.rised( - onPressed: isLoading - ? null - : () => appConfigCubit.createServerAndSetDnsRecords(), - text: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(), - ), - ], - ); - }); - } - - Widget _stepCheck(AppConfigCubit appConfigCubit) { - assert(appConfigCubit.state is AppConfigNotFinished, 'wrong state'); - var state = appConfigCubit.state as TimerState; - late int doneCount; - late String? text; - if (state.isServerResetedSecondTime) { - text = 'initializing.13'.tr(); - doneCount = 3; - } else if (state.isServerResetedFirstTime) { - text = 'initializing.21'.tr(); - doneCount = 2; - } else if (state.isServerStarted) { - text = 'initializing.14'.tr(); - doneCount = 1; - } else if (state.isServerCreated) { - text = 'initializing.15'.tr(); - doneCount = 0; - } - return Builder(builder: (context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 15), - BrandText.h4( - 'initializing.checks'.tr(args: [doneCount.toString(), "4"]), - ), - Spacer(flex: 2), - SizedBox(height: 10), - BrandText.body2(text), - SizedBox(height: 10), - if (doneCount == 0 && state.dnsMatches != null) - Column( - children: state.dnsMatches!.entries.map((entry) { - var domain = entry.key; - var isCorrect = entry.value; - return Row( - children: [ - if (isCorrect) Icon(Icons.check, color: Colors.green), - if (!isCorrect) Icon(Icons.schedule, color: Colors.amber), - SizedBox(width: 10), - Text(domain), - ], - ); - }).toList(), - ), - SizedBox(height: 10), - if (!state.isLoading) - Row( - children: [ - BrandText.body2('initializing.16'.tr()), - BrandTimer( - startDateTime: state.timerStart!, - duration: state.duration!, - ) - ], - ), - if (state.isLoading) BrandText.body2('initializing.17'.tr()), - ], - ); - }); - } - - Widget _addCard(Widget child) { - return Container( - height: 450, - padding: paddingH15V0, - child: BrandCards.big(child: child), - ); - } -} - -class _HowTo extends StatelessWidget { - const _HowTo({ - Key? key, - required this.fileName, - }) : super(key: key); - - final String fileName; - - @override - Widget build(BuildContext context) { - return BrandBottomSheet( - isExpended: true, - child: Padding( - padding: paddingH15V0, - child: BrandMarkdown( - fileName: fileName, - ), - ), - ); - } -} diff --git a/lib/ui/pages/more/about/about.dart b/lib/ui/pages/more/about/about.dart index b927c785..3d642adc 100644 --- a/lib/ui/pages/more/about/about.dart +++ b/lib/ui/pages/more/about/about.dart @@ -4,23 +4,21 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; class AboutPage extends StatelessWidget { - const AboutPage({Key? key}) : super(key: key); + const AboutPage({final super.key}); @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PreferredSize( - child: BrandHeader( - title: 'more.about_project'.tr(), hasBackButton: true), - preferredSize: Size.fromHeight(52), - ), - body: Container( - child: BrandMarkdown( + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: BrandHeader( + title: 'more.about_project'.tr(), + hasBackButton: true, + ), + ), + body: const BrandMarkdown( fileName: 'about', ), ), - ), - ); - } + ); } diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 895294c7..862815c1 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/ui/components/action_button/action_button.dart'; import 'package:selfprivacy/ui/components/brand_alert/brand_alert.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; @@ -13,35 +13,40 @@ import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:easy_localization/easy_localization.dart'; class AppSettingsPage extends StatefulWidget { - const AppSettingsPage({Key? key}) : super(key: key); + const AppSettingsPage({final super.key}); @override - _AppSettingsPageState createState() => _AppSettingsPageState(); + State createState() => _AppSettingsPageState(); } class _AppSettingsPageState extends State { @override - Widget build(BuildContext context) { - var isDarkModeOn = context.watch().state.isDarkModeOn; + Widget build(final BuildContext context) { + final bool isDarkModeOn = + context.watch().state.isDarkModeOn; return SafeArea( - child: Builder(builder: (context) { - return Scaffold( + child: Builder( + builder: (final context) => Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( - title: 'more.settings.title'.tr(), hasBackButton: true), - preferredSize: Size.fromHeight(52), + title: 'more.settings.title'.tr(), + hasBackButton: true, + ), ), body: ListView( padding: paddingH15V0, children: [ - BrandDivider(), + const BrandDivider(), Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( + border: Border( + bottom: + BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -50,12 +55,13 @@ class _AppSettingsPageState extends State { child: _TextColumn( title: 'more.settings.1'.tr(), value: 'more.settings.2'.tr(), + hasWarning: false, ), ), - SizedBox(width: 5), + const SizedBox(width: 5), BrandSwitch( value: Theme.of(context).brightness == Brightness.dark, - onChanged: (value) => context + onChanged: (final value) => context .read() .updateDarkMode(isDarkModeOn: !isDarkModeOn), ), @@ -63,11 +69,13 @@ class _AppSettingsPageState extends State { ), ), Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( + border: Border( + bottom: + BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -76,16 +84,17 @@ class _AppSettingsPageState extends State { child: _TextColumn( title: 'more.settings.3'.tr(), value: 'more.settings.4'.tr(), + hasWarning: false, ), ), - SizedBox(width: 5), + const SizedBox(width: 5), ElevatedButton( style: ElevatedButton.styleFrom( primary: BrandColors.red1, ), child: Text( 'basis.reset'.tr(), - style: TextStyle( + style: const TextStyle( color: BrandColors.white, fontWeight: NamedFontWeight.demiBold, ), @@ -93,26 +102,25 @@ class _AppSettingsPageState extends State { onPressed: () { showDialog( context: context, - builder: (_) { - return BrandAlert( - title: 'modals.3'.tr(), - contentText: 'modals.4'.tr(), - actions: [ - ActionButton( - text: 'modals.5'.tr(), - isRed: true, - onPressed: () { - context - .read() - .clearAppConfig(); - Navigator.of(context).pop(); - }), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ); - }, + builder: (final _) => BrandAlert( + title: 'modals.3'.tr(), + contentText: 'modals.4'.tr(), + actions: [ + ActionButton( + text: 'modals.5'.tr(), + isRed: true, + onPressed: () { + context + .read() + .clearAppConfig(); + Navigator.of(context).pop(); + }, + ), + ActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ), ); }, ), @@ -122,20 +130,21 @@ class _AppSettingsPageState extends State { deleteServer(context) ], ), - ); - }), + ), + ), ); } - Widget deleteServer(BuildContext context) { - var isDisabled = - context.watch().state.hetznerServer == null; + Widget deleteServer(final BuildContext context) { + final bool isDisabled = + context.watch().state.serverDetails == null; return Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -144,55 +153,57 @@ class _AppSettingsPageState extends State { child: _TextColumn( title: 'more.settings.5'.tr(), value: 'more.settings.6'.tr(), + hasWarning: false, ), ), - SizedBox(width: 5), + const SizedBox(width: 5), ElevatedButton( style: ElevatedButton.styleFrom( primary: BrandColors.red1, ), - child: Text( - 'basis.delete'.tr(), - style: TextStyle( - color: BrandColors.white, - fontWeight: NamedFontWeight.demiBold, - ), - ), onPressed: isDisabled ? null : () { showDialog( context: context, - builder: (_) { - return BrandAlert( - title: 'modals.3'.tr(), - contentText: 'modals.6'.tr(), - actions: [ - ActionButton( - text: 'modals.7'.tr(), - isRed: true, - onPressed: () async { - showDialog( - context: context, - builder: (context) { - return Container( - alignment: Alignment.center, - child: CircularProgressIndicator(), - ); - }); - await context - .read() - .serverDelete(); - Navigator.of(context).pop(); - }), - ActionButton( - text: 'basis.cancel'.tr(), - ), - ], - ); - }, + builder: (final _) => BrandAlert( + title: 'modals.3'.tr(), + contentText: 'modals.6'.tr(), + actions: [ + ActionButton( + text: 'modals.7'.tr(), + isRed: true, + onPressed: () async { + showDialog( + context: context, + builder: (final context) => Container( + alignment: Alignment.center, + child: const CircularProgressIndicator(), + ), + ); + await context + .read() + .serverDelete(); + if (!mounted) { + return; + } + Navigator.of(context).pop(); + }, + ), + ActionButton( + text: 'basis.cancel'.tr(), + ), + ], + ), ); }, + child: Text( + 'basis.delete'.tr(), + style: const TextStyle( + color: BrandColors.white, + fontWeight: NamedFontWeight.demiBold, + ), + ), ), ], ), @@ -202,32 +213,31 @@ class _AppSettingsPageState extends State { class _TextColumn extends StatelessWidget { const _TextColumn({ - Key? key, required this.title, required this.value, this.hasWarning = false, - }) : super(key: key); + }); final String title; final String value; final bool hasWarning; @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.body1( - title, - style: TextStyle(color: hasWarning ? BrandColors.warning : null), - ), - SizedBox(height: 5), - BrandText.body1(value, - style: TextStyle( + Widget build(final BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BrandText.body1( + title, + style: TextStyle(color: hasWarning ? BrandColors.warning : null), + ), + const SizedBox(height: 5), + BrandText.body1( + value, + style: const TextStyle( fontSize: 13, height: 1.53, color: BrandColors.gray1, - ).merge(TextStyle(color: hasWarning ? BrandColors.warning : null))), - ], - ); - } + ).merge(TextStyle(color: hasWarning ? BrandColors.warning : null)), + ), + ], + ); } diff --git a/lib/ui/pages/more/console/console.dart b/lib/ui/pages/more/console/console.dart index 2e129f79..76703444 100644 --- a/lib/ui/pages/more/console/console.dart +++ b/lib/ui/pages/more/console/console.dart @@ -8,10 +8,10 @@ import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; class Console extends StatefulWidget { - const Console({Key? key}) : super(key: key); + const Console({final super.key}); @override - _ConsoleState createState() => _ConsoleState(); + State createState() => _ConsoleState(); } class _ConsoleState extends State { @@ -31,71 +31,78 @@ class _ConsoleState extends State { void update() => setState(() => {}); @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PreferredSize( - child: Column( - children: [ - BrandHeader(title: 'Console', hasBackButton: true), - BrandDivider(), - ], + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(53), + child: Column( + children: const [ + BrandHeader(title: 'Console', hasBackButton: true), + BrandDivider(), + ], + ), ), - preferredSize: Size.fromHeight(53), - ), - body: FutureBuilder( - future: getIt.allReady(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - var messages = getIt.get().messages; + body: FutureBuilder( + future: getIt.allReady(), + builder: ( + final BuildContext context, + final AsyncSnapshot snapshot, + ) { + if (snapshot.hasData) { + final List messages = + getIt.get().messages; - return ListView( - reverse: true, - shrinkWrap: true, - children: [ - SizedBox(height: 20), - ...UnmodifiableListView(messages - .map((message) { - var isError = message.type == MessageType.warning; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: RichText( - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: - '${message.timeString}${isError ? '(Error)' : ''}: \n', - style: TextStyle( + return ListView( + reverse: true, + shrinkWrap: true, + children: [ + const SizedBox(height: 20), + ...UnmodifiableListView( + messages + .map((final message) { + final bool isError = + message.type == MessageType.warning; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: RichText( + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: + '${message.timeString}${isError ? '(Error)' : ''}: \n', + style: TextStyle( fontWeight: FontWeight.bold, color: - isError ? BrandColors.red1 : null)), - TextSpan(text: message.text), - ], - ), - ), - ); - }) - .toList() - .reversed), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Text('Waiting for initialisation'), - SizedBox( - height: 16, - ), - CircularProgressIndicator(), - ], - ); - } - }, + isError ? BrandColors.red1 : null, + ), + ), + TextSpan(text: message.text), + ], + ), + ), + ); + }) + .toList() + .reversed, + ), + ], + ); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: const [ + Text('Waiting for initialisation'), + SizedBox( + height: 16, + ), + CircularProgressIndicator(), + ], + ); + } + }, + ), ), - ), - ); - } + ); } diff --git a/lib/ui/pages/more/info/info.dart b/lib/ui/pages/more/info/info.dart index ef0628a6..d4c4863b 100644 --- a/lib/ui/pages/more/info/info.dart +++ b/lib/ui/pages/more/info/info.dart @@ -7,35 +7,42 @@ import 'package:package_info/package_info.dart'; import 'package:easy_localization/easy_localization.dart'; class InfoPage extends StatelessWidget { - const InfoPage({Key? key}) : super(key: key); + const InfoPage({final super.key}); @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - appBar: PreferredSize( - child: BrandHeader(title: 'more.about_app'.tr(), hasBackButton: true), - preferredSize: Size.fromHeight(52), - ), - body: ListView( - padding: paddingH15V0, - children: [ - BrandDivider(), - SizedBox(height: 10), - FutureBuilder( + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), + child: + BrandHeader(title: 'more.about_app'.tr(), hasBackButton: true), + ), + body: ListView( + padding: paddingH15V0, + children: [ + const BrandDivider(), + const SizedBox(height: 10), + FutureBuilder( future: _version(), - builder: (context, snapshot) { - return BrandText.body1('more.about_app_page.text' - .tr(args: [snapshot.data.toString()])); - }), - ], + builder: (final context, final snapshot) => BrandText.body1( + 'more.about_app_page.text' + .tr(args: [snapshot.data.toString()]), + ), + ), + ], + ), ), - ), - ); - } + ); Future _version() async { - var packageInfo = await PackageInfo.fromPlatform(); - return packageInfo.version; + String packageVersion = 'unknown'; + try { + final PackageInfo packageInfo = await PackageInfo.fromPlatform(); + packageVersion = packageInfo.version; + } catch (e) { + print(e); + } + + return packageVersion; } } diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index d87438c1..5a02da3c 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -1,36 +1,39 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:ionicons/ionicons.dart'; -import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; -import 'package:selfprivacy/ui/pages/initializing/initializing.dart'; +import 'package:selfprivacy/ui/pages/devices/devices.dart'; +import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart'; +import 'package:selfprivacy/ui/pages/setup/initializing.dart'; import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart'; -import 'package:selfprivacy/ui/pages/rootRoute.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import '../../../logic/cubit/users/users_cubit.dart'; -import 'about/about.dart'; -import 'app_settings/app_setting.dart'; -import 'console/console.dart'; -import 'info/info.dart'; +import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; +import 'package:selfprivacy/ui/pages/more/about/about.dart'; +import 'package:selfprivacy/ui/pages/more/app_settings/app_setting.dart'; +import 'package:selfprivacy/ui/pages/more/console/console.dart'; +import 'package:selfprivacy/ui/pages/more/info/info.dart'; class MorePage extends StatelessWidget { - const MorePage({Key? key}) : super(key: key); + const MorePage({final super.key}); @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.more'.tr(), - hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( children: [ @@ -38,43 +41,60 @@ class MorePage extends StatelessWidget { padding: paddingH15V0, child: Column( children: [ - BrandDivider(), - _NavItem( - title: 'more.configuration_wizard'.tr(), - iconData: BrandIcons.triangle, - goTo: InitializingPage(), - ), - _NavItem( - title: 'more.settings.title'.tr(), - iconData: BrandIcons.settings, - goTo: AppSettingsPage(), - ), - _NavItem( - title: 'more.about_project'.tr(), - iconData: BrandIcons.engineer, - goTo: AboutPage(), - ), - _NavItem( - title: 'more.about_app'.tr(), - iconData: BrandIcons.fire, - goTo: InfoPage(), - ), - _NavItem( - title: 'more.onboarding'.tr(), - iconData: BrandIcons.start, - goTo: OnboardingPage(nextPage: RootPage()), - ), - _NavItem( - title: 'more.console'.tr(), - iconData: BrandIcons.terminal, - goTo: Console(), - ), - _NavItem( + if (!isReady) + _MoreMenuItem( + title: 'more.configuration_wizard'.tr(), + iconData: Icons.change_history_outlined, + goTo: const InitializingPage(), + subtitle: 'not_ready_card.in_menu'.tr(), + accent: true, + ), + if (isReady) + _MoreMenuItem( title: 'more.create_ssh_key'.tr(), iconData: Ionicons.key_outline, goTo: SshKeysPage( user: context.read().state.rootUser, - )), + ), + ), + if (isReady) + _MoreMenuItem( + iconData: Icons.password_outlined, + goTo: const RecoveryKey(), + title: 'recovery_key.key_main_header'.tr(), + ), + if (isReady) + _MoreMenuItem( + iconData: Icons.devices_outlined, + goTo: const DevicesScreen(), + title: 'devices.main_screen.header'.tr(), + ), + _MoreMenuItem( + title: 'more.settings.title'.tr(), + iconData: Icons.settings_outlined, + goTo: const AppSettingsPage(), + ), + _MoreMenuItem( + title: 'more.about_project'.tr(), + iconData: BrandIcons.engineer, + goTo: const AboutPage(), + ), + _MoreMenuItem( + title: 'more.about_app'.tr(), + iconData: BrandIcons.fire, + goTo: const InfoPage(), + ), + if (!isReady) + _MoreMenuItem( + title: 'more.onboarding'.tr(), + iconData: BrandIcons.start, + goTo: const OnboardingPage(nextPage: RootPage()), + ), + _MoreMenuItem( + title: 'more.console'.tr(), + iconData: BrandIcons.terminal, + goTo: const Console(), + ), ], ), ) @@ -84,73 +104,52 @@ class MorePage extends StatelessWidget { } } -class _NavItem extends StatelessWidget { - const _NavItem({ - Key? key, - required this.iconData, - required this.goTo, - required this.title, - }) : super(key: key); - - final IconData iconData; - final Widget goTo; - final String title; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () => Navigator.of(context).push(materialRoute(goTo)), - child: _MoreMenuItem( - iconData: iconData, - title: title, - isActive: true, - ), - ); - } -} - class _MoreMenuItem extends StatelessWidget { const _MoreMenuItem({ - Key? key, required this.iconData, required this.title, - required this.isActive, - }) : super(key: key); + this.subtitle, + this.goTo, + this.accent = false, + }); final IconData iconData; final String title; - final bool isActive; + final Widget? goTo; + final String? subtitle; + final bool accent; @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.symmetric(vertical: 24), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - width: 1.0, - color: BrandColors.dividerColor, - ), + Widget build(final BuildContext context) { + final Color color = accent + ? Theme.of(context).colorScheme.onTertiaryContainer + : Theme.of(context).colorScheme.onSurface; + return BrandCards.filled( + tertiary: accent, + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + onTap: goTo != null + ? () => Navigator.of(context).push(materialRoute(goTo!)) + : null, + leading: Icon( + iconData, + size: 24, + color: color, ), - ), - child: Row( - children: [ - BrandText.body1( - title, - style: TextStyle( - color: isActive ? null : Colors.grey, - ), - ), - Spacer(), - SizedBox( - width: 56, - child: Icon( - iconData, - size: 20, - color: isActive ? null : Colors.grey, - ), - ), - ], + title: Text( + title, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: color, + ), + ), + subtitle: subtitle != null + ? Text( + subtitle!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: color, + ), + ) + : null, ), ); } diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 6416a5b0..dc5c8763 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -6,11 +6,11 @@ import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({Key? key, required this.nextPage}) : super(key: key); + const OnboardingPage({required this.nextPage, final super.key}); final Widget nextPage; @override - _OnboardingPageState createState() => _OnboardingPageState(); + State createState() => _OnboardingPageState(); } class _OnboardingPageState extends State { @@ -22,129 +22,121 @@ class _OnboardingPageState extends State { } @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - body: PageView( - controller: pageController, + Widget build(final BuildContext context) => SafeArea( + child: Scaffold( + body: PageView( + controller: pageController, + children: [ + _withPadding(firstPage()), + _withPadding(secondPage()), + ], + ), + ), + ); + + Widget _withPadding(final Widget child) => Padding( + padding: const EdgeInsets.symmetric( + horizontal: 15, + ), + child: child, + ); + + Widget firstPage() => ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _withPadding(firstPage()), - _withPadding(secondPage()), + const SizedBox(height: 30), + BrandText.h2( + 'onboarding.page1_title'.tr(), + ), + const SizedBox(height: 20), + BrandText.body2('onboarding.page1_text'.tr()), + Flexible( + child: Center( + child: Image.asset( + _fileName( + context: context, + path: 'assets/images/onboarding', + fileExtention: 'png', + fileName: 'onboarding1', + ), + ), + ), + ), + BrandButton.rised( + onPressed: () { + pageController.animateToPage( + 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + }, + text: 'basis.next'.tr(), + ), + const SizedBox(height: 30), ], ), - ), - ); - } + ); - Widget _withPadding(Widget child) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 15, - ), - child: child, - ); - } - - Widget firstPage() { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 30), - BrandText.h2( - 'onboarding.page1_title'.tr(), - ), - SizedBox(height: 20), - BrandText.body2('onboarding.page1_text'.tr()), - Flexible( - child: Center( + Widget secondPage() => ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height, + ), + child: Column( + children: [ + const SizedBox(height: 30), + BrandText.h2('onboarding.page2_title'.tr()), + const SizedBox(height: 20), + BrandText.body2('onboarding.page2_text'.tr()), + const SizedBox(height: 20), + Center( child: Image.asset( _fileName( context: context, path: 'assets/images/onboarding', fileExtention: 'png', - fileName: 'onboarding1', + fileName: 'logos_line', ), ), ), - ), - BrandButton.rised( - onPressed: () { - pageController.animateToPage( - 1, - duration: Duration(milliseconds: 300), - curve: Curves.easeIn, - ); - }, - text: 'basis.next'.tr(), - ), - SizedBox(height: 30), - ], - ), - ); - } - - Widget secondPage() { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height, - ), - child: Column( - children: [ - SizedBox(height: 30), - BrandText.h2('onboarding.page2_title'.tr()), - SizedBox(height: 20), - BrandText.body2('onboarding.page2_text'.tr()), - SizedBox(height: 20), - Center( - child: Image.asset( - _fileName( - context: context, - path: 'assets/images/onboarding', - fileExtention: 'png', - fileName: 'logos_line', - ), - ), - ), - Flexible( - child: Center( - child: Image.asset( - _fileName( - context: context, - path: 'assets/images/onboarding', - fileExtention: 'png', - fileName: 'onboarding2', + Flexible( + child: Center( + child: Image.asset( + _fileName( + context: context, + path: 'assets/images/onboarding', + fileExtention: 'png', + fileName: 'onboarding2', + ), ), ), ), - ), - BrandButton.rised( - onPressed: () { - context.read().turnOffOnboarding(); - Navigator.of(context).pushAndRemoveUntil( - materialRoute(widget.nextPage), - (route) => false, - ); - }, - text: 'basis.got_it'.tr(), - ), - SizedBox(height: 30), - ], - ), - ); - } + BrandButton.rised( + onPressed: () { + context.read().turnOffOnboarding(); + Navigator.of(context).pushAndRemoveUntil( + materialRoute(widget.nextPage), + (final route) => false, + ); + }, + text: 'basis.got_it'.tr(), + ), + const SizedBox(height: 30), + ], + ), + ); } String _fileName({ - required BuildContext context, - required String path, - required String fileName, - required String fileExtention, + required final BuildContext context, + required final String path, + required final String fileName, + required final String fileExtention, }) { - var theme = Theme.of(context); - var isDark = theme.brightness == Brightness.dark; + final ThemeData theme = Theme.of(context); + final bool isDark = theme.brightness == Brightness.dark; return '$path/$fileName${isDark ? '-dark' : '-light'}.$fileExtention'; } diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index dc53ed2f..97e4aeeb 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/provider.dart'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; @@ -15,24 +15,27 @@ import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart'; import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart'; -import 'package:selfprivacy/ui/pages/server_details/server_details.dart'; +import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -var navigatorKey = GlobalKey(); +GlobalKey navigatorKey = GlobalKey(); class ProvidersPage extends StatefulWidget { - ProvidersPage({Key? key}) : super(key: key); + const ProvidersPage({final super.key}); @override - _ProvidersPageState createState() => _ProvidersPageState(); + State createState() => _ProvidersPageState(); } class _ProvidersPageState extends State { @override - Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; - var isBackupInitialized = context.watch().state.isInitialized; - var dnsStatus = context.watch().state.dnsState; + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + final bool isBackupInitialized = + context.watch().state.isInitialized; + final DnsRecordsStatus dnsStatus = + context.watch().state.dnsState; StateType getDnsStatus() { if (dnsStatus == DnsRecordsStatus.uninitialized || @@ -45,10 +48,10 @@ class _ProvidersPageState extends State { return StateType.stable; } - final cards = ProviderType.values + final List cards = ProviderType.values .map( - (type) => Padding( - padding: EdgeInsets.only(bottom: 30), + (final ProviderType type) => Padding( + padding: const EdgeInsets.only(bottom: 30), child: _Card( provider: ProviderModel( state: isReady @@ -66,18 +69,17 @@ class _ProvidersPageState extends State { .toList(); return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'providers.page_title'.tr(), - hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( padding: paddingH15V0, children: [ if (!isReady) ...[ - NotReadyCard(), - SizedBox(height: 24), + const NotReadyCard(), + const SizedBox(height: 24), ], ...cards, ], @@ -87,20 +89,22 @@ class _ProvidersPageState extends State { } class _Card extends StatelessWidget { - const _Card({Key? key, required this.provider}) : super(key: key); + const _Card({required this.provider}); final ProviderModel provider; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { late String title; String? message; late String stableText; late VoidCallback onTap; - var isReady = context.watch().state is AppConfigFinished; - AppConfigState appConfig = context.watch().state; + final bool isReady = context.watch().state + is ServerInstallationFinished; + final ServerInstallationState appConfig = + context.watch().state; - var domainName = - appConfig.isDomainFilled ? appConfig.cloudFlareDomain!.domainName : ''; + final String domainName = + appConfig.isDomainFilled ? appConfig.serverDomain!.domainName : ''; switch (provider.type) { case ProviderType.server: @@ -108,9 +112,9 @@ class _Card extends StatelessWidget { stableText = 'providers.server.status'.tr(); onTap = () => showBrandBottomSheet( context: context, - builder: (context) => BrandBottomSheet( + builder: (final BuildContext context) => const BrandBottomSheet( isExpended: true, - child: ServerDetails(), + child: ServerDetailsScreen(), ), ); @@ -120,17 +124,21 @@ class _Card extends StatelessWidget { message = domainName; stableText = 'providers.domain.status'.tr(); - onTap = () => Navigator.of(context).push(materialRoute( - DnsDetailsPage(), - )); + onTap = () => Navigator.of(context).push( + materialRoute( + const DnsDetailsPage(), + ), + ); break; case ProviderType.backup: title = 'providers.backup.card_title'.tr(); stableText = 'providers.backup.status'.tr(); - onTap = () => Navigator.of(context).push(materialRoute( - BackupDetails(), - )); + onTap = () => Navigator.of(context).push( + materialRoute( + const BackupDetails(), + ), + ); break; } return GestureDetector( @@ -143,12 +151,12 @@ class _Card extends StatelessWidget { status: provider.state, child: Icon(provider.icon, size: 30, color: Colors.white), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2(title), - SizedBox(height: 10), + const SizedBox(height: 10), if (message != null) ...[ BrandText.body2(message), - SizedBox(height: 10), + const SizedBox(height: 10), ], if (provider.state == StateType.stable) BrandText.body2(stableText), ], diff --git a/lib/ui/pages/recovery_key/recovery_key.dart b/lib/ui/pages/recovery_key/recovery_key.dart new file mode 100644 index 00000000..44147f57 --- /dev/null +++ b/lib/ui/pages/recovery_key/recovery_key.dart @@ -0,0 +1,414 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/logic/cubit/recovery_key/recovery_key_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/recovery_key/recovery_key_receiving.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class RecoveryKey extends StatefulWidget { + const RecoveryKey({final super.key}); + + @override + State createState() => _RecoveryKeyState(); +} + +class _RecoveryKeyState extends State { + @override + Widget build(final BuildContext context) { + final RecoveryKeyState keyStatus = context.watch().state; + + final List widgets; + String? subtitle = + keyStatus.exists ? null : 'recovery_key.key_main_description'.tr(); + + switch (keyStatus.loadingStatus) { + case LoadingStatus.refreshing: + subtitle = 'recovery_key.key_synchronizing'.tr(); + widgets = [ + const Center(child: CircularProgressIndicator()), + ]; + break; + case LoadingStatus.success: + widgets = [ + const RecoveryKeyContent(), + ]; + break; + case LoadingStatus.uninitialized: + case LoadingStatus.error: + subtitle = 'recovery_key.key_connection_error'.tr(); + widgets = [ + const Icon(Icons.sentiment_dissatisfied_outlined), + ]; + break; + } + + return BrandHeroScreen( + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: subtitle, + hasBackButton: true, + hasFlashButton: false, + children: widgets, + ); + } +} + +class RecoveryKeyContent extends StatefulWidget { + const RecoveryKeyContent({final super.key}); + + @override + State createState() => _RecoveryKeyContentState(); +} + +class _RecoveryKeyContentState extends State { + bool _isConfigurationVisible = false; + + @override + Widget build(final BuildContext context) { + final RecoveryKeyState keyStatus = context.watch().state; + + return Column( + children: [ + if (keyStatus.exists) RecoveryKeyStatusCard(isValid: keyStatus.isValid), + const SizedBox(height: 16), + if (keyStatus.exists && !_isConfigurationVisible) + RecoveryKeyInformation(state: keyStatus), + if (_isConfigurationVisible || !keyStatus.exists) + const RecoveryKeyConfiguration(), + const SizedBox(height: 16), + if (!_isConfigurationVisible && keyStatus.isValid && keyStatus.exists) + BrandButton.text( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () { + setState(() { + _isConfigurationVisible = true; + }); + }, + ), + if (!_isConfigurationVisible && !keyStatus.isValid && keyStatus.exists) + FilledButton( + title: 'recovery_key.key_replace_button'.tr(), + onPressed: () { + setState(() { + _isConfigurationVisible = true; + }); + }, + ), + ], + ); + } +} + +class RecoveryKeyStatusCard extends StatelessWidget { + const RecoveryKeyStatusCard({required this.isValid, final super.key}); + + final bool isValid; + + @override + Widget build(final BuildContext context) => BrandCards.filled( + child: ListTile( + title: isValid + ? Text( + 'recovery_key.key_valid'.tr(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + : Text( + 'recovery_key.key_invalid'.tr(), + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onErrorContainer, + ), + ), + leading: isValid + ? Icon( + Icons.check_circle_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ) + : Icon( + Icons.cancel_outlined, + color: Theme.of(context).colorScheme.onErrorContainer, + ), + tileColor: isValid + ? Theme.of(context).colorScheme.surfaceVariant + : Theme.of(context).colorScheme.errorContainer, + ), + ); +} + +class RecoveryKeyInformation extends StatelessWidget { + const RecoveryKeyInformation({required this.state, final super.key}); + + final RecoveryKeyState state; + + @override + Widget build(final BuildContext context) { + const EdgeInsets padding = + EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0); + return SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.expiresAt != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_valid_until'.tr( + args: [DateFormat.yMMMMd().format(state.expiresAt!)], + ), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: state.isInvalidBecauseExpired + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onBackground, + ), + ), + ), + if (state.usesLeft != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_valid_for'.tr( + args: [state.usesLeft!.toString()], + ), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: state.isInvalidBecauseUsed + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.onBackground, + ), + ), + ), + if (state.generatedAt != null) + Padding( + padding: padding, + child: Text( + 'recovery_key.key_creation_date'.tr( + args: [DateFormat.yMMMMd().format(state.generatedAt!)], + ), + ), + ), + ], + ), + ); + } +} + +class RecoveryKeyConfiguration extends StatefulWidget { + const RecoveryKeyConfiguration({final super.key}); + + @override + State createState() => _RecoveryKeyConfigurationState(); +} + +class _RecoveryKeyConfigurationState extends State { + bool _isAmountToggled = false; + bool _isExpirationToggled = false; + + final TextEditingController _amountController = TextEditingController(); + final TextEditingController _expirationController = TextEditingController(); + + bool _isAmountError = false; + bool _isExpirationError = false; + + DateTime _selectedDate = DateTime.now(); + bool _isDateSelected = false; + + bool _isLoading = false; + + Future _generateRecoveryToken() async { + setState(() { + _isLoading = true; + }); + try { + final String token = + await context.read().generateRecoveryKey( + numberOfUses: _isAmountToggled + ? int.tryParse(_amountController.text) + : null, + expirationDate: _isExpirationToggled ? _selectedDate : null, + ); + if (!mounted) { + return; + } + setState(() { + _isLoading = false; + }); + Navigator.of(context).push( + materialRoute( + RecoveryKeyReceiving(recoveryKey: token), // TO DO + ), + ); + } on GenerationError catch (e) { + setState(() { + _isLoading = false; + }); + getIt().showSnackBar( + 'recovery_key.generation_error'.tr(args: [e.message]), + ); + return; + } + } + + void _updateErrorStatuses() { + final String amount = _amountController.text; + final String expiration = _expirationController.text; + + print('amount: $amount'); + print('_isAmountToggled: $_isAmountToggled'); + print('_isExpirationToggled: $_isExpirationToggled'); + + setState(() { + if (!_isAmountToggled) { + _isAmountError = false; + } else if (amount.isEmpty) { + _isAmountError = true; + } else { + final int? amountInt = int.tryParse(amount); + _isAmountError = amountInt == null || amountInt <= 0; + } + + if (!_isExpirationToggled) { + _isExpirationError = false; + } else if (expiration.isEmpty) { + _isExpirationError = true; + } else { + _isExpirationError = _selectedDate.isBefore(DateTime.now()); + } + }); + + print('_isAmountError: $_isAmountError'); + print('_isExpirationError: $_isExpirationError'); + } + + @override + Widget build(final BuildContext context) { + if (_isDateSelected) { + _expirationController.text = DateFormat.yMMMMd().format(_selectedDate); + } + + _amountController.addListener(_updateErrorStatuses); + + _expirationController.addListener(_updateErrorStatuses); + + return Column( + children: [ + SwitchListTile( + value: _isAmountToggled, + title: Text('recovery_key.key_amount_toggle'.tr()), + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (final bool toggled) { + setState( + () { + _isAmountToggled = toggled; + }, + ); + _updateErrorStatuses(); + }, + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 200), + crossFadeState: _isAmountToggled + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Column( + children: [ + const SizedBox(height: 8), + TextField( + enabled: _isAmountToggled, + controller: _amountController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: _isAmountError ? ' ' : null, + labelText: 'recovery_key.key_amount_field_title'.tr(), + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + const SizedBox(height: 8), + ], + ), + secondChild: Container(), + ), + SwitchListTile( + value: _isExpirationToggled, + title: Text('recovery_key.key_duedate_toggle'.tr()), + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (final bool toggled) { + setState( + () { + _isExpirationToggled = toggled; + }, + ); + _updateErrorStatuses(); + }, + ), + AnimatedCrossFade( + duration: const Duration(milliseconds: 200), + crossFadeState: _isExpirationToggled + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: Column( + children: [ + const SizedBox(height: 8), + TextField( + enabled: _isExpirationToggled, + controller: _expirationController, + onTap: () { + _selectDate(context); + }, + readOnly: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: _isExpirationError ? ' ' : null, + labelText: 'recovery_key.key_duedate_field_title'.tr(), + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], // Only numbers can be entered + ), + ], + ), + secondChild: Container(), + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovery_key.key_receive_button'.tr(), + disabled: _isAmountError || _isExpirationError || _isLoading, + onPressed: !_isAmountError && !_isExpirationError + ? _generateRecoveryToken + : null, + ), + ], + ); + } + + Future _selectDate(final BuildContext context) async { + final DateTime? selected = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 50), + ); + + if (selected != null && selected != _selectedDate) { + setState( + () { + _selectedDate = selected; + _isDateSelected = true; + }, + ); + } + + return _selectedDate; + } +} diff --git a/lib/ui/pages/recovery_key/recovery_key_receiving.dart b/lib/ui/pages/recovery_key/recovery_key_receiving.dart new file mode 100644 index 00000000..7ae6adaf --- /dev/null +++ b/lib/ui/pages/recovery_key/recovery_key_receiving.dart @@ -0,0 +1,48 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class RecoveryKeyReceiving extends StatelessWidget { + const RecoveryKeyReceiving({required this.recoveryKey, final super.key}); + + final String recoveryKey; + + @override + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'recovery_key.key_main_header'.tr(), + heroSubtitle: 'recovery_key.key_receiving_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + const Divider(), + const SizedBox(height: 16), + Text( + recoveryKey, + style: Theme.of(context).textTheme.bodyLarge!.copyWith( + fontSize: 24, + fontFamily: 'RobotoMono', + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.info_outlined, size: 24), + const SizedBox(height: 16), + Text('recovery_key.key_receiving_info'.tr()), + ], + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovery_key.key_receiving_done'.tr(), + onPressed: () { + Navigator.of(context).popUntil((final route) => route.isFirst); + }, + ), + ], + ); +} diff --git a/lib/ui/pages/rootRoute.dart b/lib/ui/pages/rootRoute.dart deleted file mode 100644 index 74a28880..00000000 --- a/lib/ui/pages/rootRoute.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:selfprivacy/logic/api_maps/server.dart'; -import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart'; -import 'package:selfprivacy/ui/pages/more/more.dart'; -import 'package:selfprivacy/ui/pages/providers/providers.dart'; -import 'package:selfprivacy/ui/pages/services/services.dart'; -import 'package:selfprivacy/ui/pages/users/users.dart'; - -class RootPage extends StatefulWidget { - const RootPage({Key? key}) : super(key: key); - - @override - _RootPageState createState() => _RootPageState(); -} - -class _RootPageState extends State - with SingleTickerProviderStateMixin { - late TabController tabController; - - @override - void initState() { - tabController = TabController(length: 4, vsync: this); - super.initState(); - } - - @override - void dispose() { - super.dispose(); - tabController.dispose(); - } - - var selfprivacyServer = ServerApi(); - - @override - Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( - body: Provider( - create: (_) => ChangeTab(tabController.animateTo), - child: TabBarView( - controller: tabController, - children: [ - ProvidersPage(), - ServicesPage(), - UsersPage(), - MorePage(), - ], - ), - ), - bottomNavigationBar: BrandTabBar( - controller: tabController, - ), - ), - ); - } -} - -class ChangeTab { - final ValueChanged onPress; - - ChangeTab(this.onPress); -} diff --git a/lib/ui/pages/root_route.dart b/lib/ui/pages/root_route.dart new file mode 100644 index 00000000..d68e4a0e --- /dev/null +++ b/lib/ui/pages/root_route.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_tab_bar/brand_tab_bar.dart'; +import 'package:selfprivacy/ui/pages/more/more.dart'; +import 'package:selfprivacy/ui/pages/providers/providers.dart'; +import 'package:selfprivacy/ui/pages/services/services.dart'; +import 'package:selfprivacy/ui/pages/users/users.dart'; + +import 'package:selfprivacy/ui/components/pre_styled_buttons/flash_fab.dart'; + +class RootPage extends StatefulWidget { + const RootPage({final super.key}); + + @override + State createState() => _RootPageState(); +} + +class _RootPageState extends State with TickerProviderStateMixin { + late TabController tabController; + + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 400), + vsync: this, + ); + late final Animation _animation = CurvedAnimation( + parent: _controller, + curve: Curves.fastOutSlowIn, + ); + + @override + void initState() { + tabController = TabController(length: 4, vsync: this); + tabController.addListener(() { + setState(() { + tabController.index == 2 + ? _controller.forward() + : _controller.reverse(); + }); + }); + super.initState(); + } + + @override + void dispose() { + tabController.dispose(); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + + return SafeArea( + child: Provider( + create: (final _) => ChangeTab(tabController.animateTo), + child: Scaffold( + body: TabBarView( + controller: tabController, + children: const [ + ProvidersPage(), + ServicesPage(), + UsersPage(), + MorePage(), + ], + ), + bottomNavigationBar: BrandTabBar( + controller: tabController, + ), + floatingActionButton: isReady + ? SizedBox( + height: 104 + 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ScaleTransition( + scale: _animation, + child: const AddUserFab(), + ), + const SizedBox(height: 16), + const BrandFab(), + ], + ), + ) + : null, + ), + ), + ); + } +} + +class ChangeTab { + ChangeTab(this.onPress); + final ValueChanged onPress; +} diff --git a/lib/ui/pages/server_details/chart.dart b/lib/ui/pages/server_details/chart.dart index 6ccd86fc..f7972e9c 100644 --- a/lib/ui/pages/server_details/chart.dart +++ b/lib/ui/pages/server_details/chart.dart @@ -1,13 +1,11 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _Chart extends StatelessWidget { - const _Chart({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - var cubit = context.watch(); - var period = cubit.state.period; - var state = cubit.state; + Widget build(final BuildContext context) { + final HetznerMetricsCubit cubit = context.watch(); + final Period period = cubit.state.period; + final HetznerMetricsState state = cubit.state; List charts; if (state is HetznerMetricsLoading) { charts = [ @@ -19,33 +17,33 @@ class _Chart extends StatelessWidget { ]; } else if (state is HetznerMetricsLoaded) { charts = [ - Legend(color: Colors.red, text: 'CPU %'), - SizedBox(height: 20), + const Legend(color: Colors.red, text: 'CPU %'), + const SizedBox(height: 20), getCpuChart(state), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ BrandText.small('Public Network interface packets per sec'), - SizedBox(width: 10), - Legend(color: Colors.red, text: 'IN'), - SizedBox(width: 5), - Legend(color: Colors.green, text: 'OUT'), + const SizedBox(width: 10), + const Legend(color: Colors.red, text: 'IN'), + const SizedBox(width: 5), + const Legend(color: Colors.green, text: 'OUT'), ], ), - SizedBox(height: 20), + const SizedBox(height: 20), getPpsChart(state), - SizedBox(height: 1), + const SizedBox(height: 1), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ BrandText.small('Public Network interface bytes per sec'), - SizedBox(width: 10), - Legend(color: Colors.red, text: 'IN'), - SizedBox(width: 5), - Legend(color: Colors.green, text: 'OUT'), + const SizedBox(width: 10), + const Legend(color: Colors.red, text: 'IN'), + const SizedBox(width: 5), + const Legend(color: Colors.green, text: 'OUT'), ], ), - SizedBox(height: 20), + const SizedBox(height: 20), getBandwidthChart(state), ]; } else { @@ -57,7 +55,7 @@ class _Chart extends StatelessWidget { child: Column( children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 20.0, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -85,39 +83,43 @@ class _Chart extends StatelessWidget { ); } - Widget getCpuChart(HetznerMetricsLoaded state) { - var data = state.cpu; + Widget getCpuChart(final HetznerMetricsLoaded state) { + final data = state.cpu; - return Container( + return SizedBox( height: 200, - child: CpuChart(data, state.period, state.start), - ); - } - - Widget getPpsChart(HetznerMetricsLoaded state) { - var ppsIn = state.ppsIn; - var ppsOut = state.ppsOut; - - return Container( - height: 200, - child: NetworkChart( - [ppsIn, ppsOut], - state.period, - state.start, + child: CpuChart( + data: data, + period: state.period, + start: state.start, ), ); } - Widget getBandwidthChart(HetznerMetricsLoaded state) { - var ppsIn = state.bandwidthIn; - var ppsOut = state.bandwidthOut; + Widget getPpsChart(final HetznerMetricsLoaded state) { + final ppsIn = state.ppsIn; + final ppsOut = state.ppsOut; - return Container( + return SizedBox( height: 200, child: NetworkChart( - [ppsIn, ppsOut], - state.period, - state.start, + listData: [ppsIn, ppsOut], + period: state.period, + start: state.start, + ), + ); + } + + Widget getBandwidthChart(final HetznerMetricsLoaded state) { + final ppsIn = state.bandwidthIn; + final ppsOut = state.bandwidthOut; + + return SizedBox( + height: 200, + child: NetworkChart( + listData: [ppsIn, ppsOut], + period: state.period, + start: state.start, ), ); } @@ -125,44 +127,40 @@ class _Chart extends StatelessWidget { class Legend extends StatelessWidget { const Legend({ - Key? key, required this.color, required this.text, - }) : super(key: key); + final super.key, + }); final String text; final Color color; @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _ColoredBox(color: color), - SizedBox(width: 5), - BrandText.small(text), - ], - ); - } + Widget build(final BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _ColoredBox(color: color), + const SizedBox(width: 5), + BrandText.small(text), + ], + ); } class _ColoredBox extends StatelessWidget { const _ColoredBox({ - Key? key, required this.color, - }) : super(key: key); + }); final Color color; @override - Widget build(BuildContext context) { - return Container( - width: 10, - height: 10, - decoration: BoxDecoration( + Widget build(final BuildContext context) => Container( + width: 10, + height: 10, + decoration: BoxDecoration( color: color.withOpacity(0.3), border: Border.all( color: color, - )), - ); - } + ), + ), + ); } diff --git a/lib/ui/pages/server_details/cpu_chart.dart b/lib/ui/pages/server_details/cpu_chart.dart index fd131e61..11f1eaef 100644 --- a/lib/ui/pages/server_details/cpu_chart.dart +++ b/lib/ui/pages/server_details/cpu_chart.dart @@ -7,7 +7,12 @@ import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:intl/intl.dart'; class CpuChart extends StatelessWidget { - CpuChart(this.data, this.period, this.start); + const CpuChart({ + required this.data, + required this.period, + required this.start, + final super.key, + }); final List data; final Period period; @@ -15,9 +20,9 @@ class CpuChart extends StatelessWidget { List getSpots() { var i = 0; - List res = []; + final List res = []; - for (var d in data) { + for (final d in data) { res.add(FlSpot(i.toDouble(), d.value)); i++; } @@ -26,97 +31,96 @@ class CpuChart extends StatelessWidget { } @override - Widget build(BuildContext context) { - return LineChart( - LineChartData( - lineTouchData: LineTouchData(enabled: false), - lineBarsData: [ - LineChartBarData( - spots: getSpots(), - isCurved: true, - barWidth: 1, - color: Colors.red, - dotData: FlDotData( - show: false, + Widget build(final BuildContext context) => LineChart( + LineChartData( + lineTouchData: LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: getSpots(), + isCurved: true, + barWidth: 1, + color: Colors.red, + dotData: FlDotData( + show: false, + ), ), - ), - ], - minY: 0, - maxY: 100, - minX: data.length - 200, - titlesData: FlTitlesData( - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 20, - reservedSize: 50, - getTitlesWidget: (value, titleMeta) { - return Padding( + ], + minY: 0, + maxY: 100, + minX: data.length - 200, + titlesData: FlTitlesData( + topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + interval: 20, + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => Padding( padding: const EdgeInsets.all(8.0), child: RotatedBox( - child: Text(bottomTitle(value.toInt()), - style: TextStyle( - fontSize: 10, - color: Colors.purple, - fontWeight: FontWeight.bold, - )), quarterTurns: 1, - ), - ); - }, - showTitles: true, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - getTitlesWidget: (value, titleMeta) { - return Padding( - padding: EdgeInsets.only(right: 15), child: Text( - value.toInt().toString(), - style: progressTextStyleLight.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.gray4 - : null, + bottomTitle(value.toInt()), + style: const TextStyle( + fontSize: 10, + color: Colors.purple, + fontWeight: FontWeight.bold, ), - )); - }, - interval: 25, - showTitles: false, + ), + ), + ), + showTitles: true, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + getTitlesWidget: (final value, final titleMeta) => Padding( + padding: const EdgeInsets.only(right: 15), + child: Text( + value.toInt().toString(), + style: progressTextStyleLight.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? BrandColors.gray4 + : null, + ), + ), + ), + interval: 25, + showTitles: false, + ), ), ), + gridData: FlGridData(show: true), ), - gridData: FlGridData(show: true), - ), - ); - } + ); bool checkToShowTitle( - double minValue, - double maxValue, - SideTitles sideTitles, - double appliedInterval, - double value, + final double minValue, + final double maxValue, + final SideTitles sideTitles, + final double appliedInterval, + final double value, ) { if (value < 0) { return false; } else if (value == 0) { return true; } - var _value = value - minValue; - var v = _value / 20; + + final localValue = value - minValue; + final v = localValue / 20; return v - v.floor() == 0; } - String bottomTitle(int value) { + String bottomTitle(final int value) { final hhmm = DateFormat('HH:mm'); - var day = DateFormat('MMMd'); + final day = DateFormat('MMMd'); String res; if (value <= 0) { return ''; } - var time = data[value].time; + + final time = data[value].time; switch (period) { case Period.hour: case Period.day: diff --git a/lib/ui/pages/server_details/header.dart b/lib/ui/pages/server_details/header.dart index d03d3d08..a10bc1cf 100644 --- a/lib/ui/pages/server_details/header.dart +++ b/lib/ui/pages/server_details/header.dart @@ -1,61 +1,58 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _Header extends StatelessWidget { const _Header({ - Key? key, required this.providerState, required this.tabController, - }) : super(key: key); + }); final StateType providerState; final TabController tabController; @override - Widget build(BuildContext context) { - return Row( - children: [ - IconStatusMask( - status: providerState, - child: Icon( - BrandIcons.server, - size: 40, - color: Colors.white, - ), - ), - SizedBox(width: 10), - BrandText.h2('providers.server.card_title'.tr()), - Spacer(), - Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: PopupMenuButton<_PopupMenuItemType>( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), + Widget build(final BuildContext context) => Row( + children: [ + IconStatusMask( + status: providerState, + child: const Icon( + BrandIcons.server, + size: 40, + color: Colors.white, ), - onSelected: (_PopupMenuItemType result) { - switch (result) { - case _PopupMenuItemType.setting: - tabController.animateTo(1); - break; - } - }, - icon: Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - PopupMenuItem<_PopupMenuItemType>( - value: _PopupMenuItemType.setting, - child: Container( - padding: EdgeInsets.only(left: 5), - child: Text('basis.settings'.tr()), - ), - ), - ], ), - ), - ], - ); - } + const SizedBox(width: 10), + BrandText.h2('providers.server.card_title'.tr()), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 2, + ), + child: PopupMenuButton<_PopupMenuItemType>( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + onSelected: (final _PopupMenuItemType result) { + switch (result) { + case _PopupMenuItemType.setting: + tabController.animateTo(1); + break; + } + }, + icon: const Icon(Icons.more_vert), + itemBuilder: (final BuildContext context) => [ + PopupMenuItem<_PopupMenuItemType>( + value: _PopupMenuItemType.setting, + child: Container( + padding: const EdgeInsets.only(left: 5), + child: Text('basis.settings'.tr()), + ), + ), + ], + ), + ), + ], + ); } enum _PopupMenuItemType { setting } diff --git a/lib/ui/pages/server_details/network_charts.dart b/lib/ui/pages/server_details/network_charts.dart index 8827eb9f..d1375ae6 100644 --- a/lib/ui/pages/server_details/network_charts.dart +++ b/lib/ui/pages/server_details/network_charts.dart @@ -9,21 +9,22 @@ import 'package:selfprivacy/logic/models/hetzner_metrics.dart'; import 'package:intl/intl.dart'; class NetworkChart extends StatelessWidget { - NetworkChart( - this.listData, - this.period, - this.start, - ); + const NetworkChart({ + required this.listData, + required this.period, + required this.start, + final super.key, + }); final List> listData; final Period period; final DateTime start; - List getSpots(data) { + List getSpots(final data) { var i = 0; - List res = []; + final List res = []; - for (var d in data) { + for (final d in data) { res.add(FlSpot(i.toDouble(), d.value)); i++; } @@ -32,120 +33,119 @@ class NetworkChart extends StatelessWidget { } @override - Widget build(BuildContext context) { - return SizedBox( - height: 150, - width: MediaQuery.of(context).size.width, - child: LineChart( - LineChartData( - lineTouchData: LineTouchData(enabled: false), - lineBarsData: [ - LineChartBarData( - spots: getSpots(listData[0]), - isCurved: true, - barWidth: 1, - color: Colors.red, - dotData: FlDotData( - show: false, + Widget build(final BuildContext context) => SizedBox( + height: 150, + width: MediaQuery.of(context).size.width, + child: LineChart( + LineChartData( + lineTouchData: LineTouchData(enabled: false), + lineBarsData: [ + LineChartBarData( + spots: getSpots(listData[0]), + isCurved: true, + barWidth: 1, + color: Colors.red, + dotData: FlDotData( + show: false, + ), ), - ), - LineChartBarData( - spots: getSpots(listData[1]), - isCurved: true, - barWidth: 1, - color: Colors.green, - dotData: FlDotData( - show: false, + LineChartBarData( + spots: getSpots(listData[1]), + isCurved: true, + barWidth: 1, + color: Colors.green, + dotData: FlDotData( + show: false, + ), ), - ), - ], - minY: 0, - maxY: [ - ...listData[0].map((e) => e.value), - ...listData[1].map((e) => e.value) - ].reduce(max) * - 1.2, - minX: listData[0].length - 200, - titlesData: FlTitlesData( - topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - interval: 20, - reservedSize: 50, - getTitlesWidget: (value, titleMeta) { - return Padding( + ], + minY: 0, + maxY: [ + ...listData[0].map((final e) => e.value), + ...listData[1].map((final e) => e.value) + ].reduce(max) * + 1.2, + minX: listData[0].length - 200, + titlesData: FlTitlesData( + topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + interval: 20, + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => Padding( padding: const EdgeInsets.all(8.0), child: RotatedBox( - child: Text(bottomTitle(value.toInt()), - style: TextStyle( - fontSize: 10, - color: Colors.purple, - fontWeight: FontWeight.bold, - )), quarterTurns: 1, - ), - ); - }, - showTitles: true, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - reservedSize: 50, - getTitlesWidget: (value, titleMeta) { - return Padding( - padding: EdgeInsets.only(right: 5), child: Text( - value.toInt().toString(), - style: progressTextStyleLight.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? BrandColors.gray4 - : null, + bottomTitle(value.toInt()), + style: const TextStyle( + fontSize: 10, + color: Colors.purple, + fontWeight: FontWeight.bold, ), - )); - }, - interval: [ - ...listData[0].map((e) => e.value), - ...listData[1].map((e) => e.value) - ].reduce(max) * - 2 / - 10, - showTitles: false, + ), + ), + ), + showTitles: true, + ), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + reservedSize: 50, + getTitlesWidget: (final value, final titleMeta) => Padding( + padding: const EdgeInsets.only(right: 5), + child: Text( + value.toInt().toString(), + style: progressTextStyleLight.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? BrandColors.gray4 + : null, + ), + ), + ), + interval: [ + ...listData[0].map((final e) => e.value), + ...listData[1].map((final e) => e.value) + ].reduce(max) * + 2 / + 10, + showTitles: false, + ), ), ), + gridData: FlGridData(show: true), ), - gridData: FlGridData(show: true), ), - ), - ); - } + ); bool checkToShowTitle( - double minValue, - double maxValue, - SideTitles sideTitles, - double appliedInterval, - double value, + final double minValue, + final double maxValue, + final SideTitles sideTitles, + final double appliedInterval, + final double value, ) { if (value < 0) { return false; } else if (value == 0) { return true; } - var _value = value - minValue; - var v = _value / 20; - return v - v.floor() == 0; + + final diff = value - minValue; + final finalValue = diff / 20; + return finalValue - finalValue.floor() == 0; } - String bottomTitle(int value) { + String bottomTitle(final int value) { final hhmm = DateFormat('HH:mm'); - var day = DateFormat('MMMd'); + final day = DateFormat('MMMd'); String res; if (value <= 0) { return ''; } - var time = listData[0][value].time; + + final time = listData[0][value].time; switch (period) { case Period.hour: case Period.day: diff --git a/lib/ui/pages/server_details/server_details.dart b/lib/ui/pages/server_details/server_details_screen.dart similarity index 70% rename from lib/ui/pages/server_details/server_details.dart rename to lib/ui/pages/server_details/server_details_screen.dart index 5368df30..1f442220 100644 --- a/lib/ui/pages/server_details/server_details.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -1,11 +1,12 @@ import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/hetzner_metrics/hetzner_metrics_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; @@ -14,32 +15,32 @@ import 'package:selfprivacy/ui/components/brand_loader/brand_loader.dart'; import 'package:selfprivacy/ui/components/brand_radio_tile/brand_radio_tile.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/ui/components/switch_block/switch_bloc.dart'; import 'package:selfprivacy/ui/pages/server_details/time_zone/lang.dart'; +import 'package:selfprivacy/utils/extensions/duration.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:timezone/timezone.dart'; -import 'cpu_chart.dart'; -import 'network_charts.dart'; -import 'package:selfprivacy/utils/extensions/duration.dart'; -part 'server_settings.dart'; -part 'text_details.dart'; +import 'package:selfprivacy/ui/pages/server_details/cpu_chart.dart'; +import 'package:selfprivacy/ui/pages/server_details/network_charts.dart'; + part 'chart.dart'; part 'header.dart'; +part 'server_settings.dart'; +part 'text_details.dart'; part 'time_zone/time_zone.dart'; var navigatorKey = GlobalKey(); -class ServerDetails extends StatefulWidget { - const ServerDetails({Key? key}) : super(key: key); +class ServerDetailsScreen extends StatefulWidget { + const ServerDetailsScreen({final super.key}); @override - _ServerDetailsState createState() => _ServerDetailsState(); + State createState() => _ServerDetailsScreenState(); } -class _ServerDetailsState extends State +class _ServerDetailsScreenState extends State with SingleTickerProviderStateMixin { late TabController tabController; @@ -59,33 +60,34 @@ class _ServerDetailsState extends State } @override - Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; - var providerState = isReady ? StateType.stable : StateType.uninitialized; + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; + final providerState = isReady ? StateType.stable : StateType.uninitialized; return BlocProvider( - create: (context) => ServerDetailsCubit()..check(), + create: (final context) => ServerDetailsCubit()..check(), child: Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: Column( children: [ Container( height: 51, alignment: Alignment.center, - padding: EdgeInsets.symmetric(horizontal: 15), + padding: const EdgeInsets.symmetric(horizontal: 15), child: BrandText.h4('basis.details'.tr()), ), - BrandDivider(), + const BrandDivider(), ], ), - preferredSize: Size.fromHeight(52), ), body: TabBarView( - physics: NeverScrollableScrollPhysics(), + physics: const NeverScrollableScrollPhysics(), controller: tabController, children: [ SingleChildScrollView( - physics: ClampingScrollPhysics(), + physics: const ClampingScrollPhysics(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -95,18 +97,19 @@ class _ServerDetailsState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ _Header( - providerState: providerState, - tabController: tabController), + providerState: providerState, + tabController: tabController, + ), BrandText.body1('providers.server.bottom_sheet.1'.tr()), ], ), ), - SizedBox(height: 10), + const SizedBox(height: 10), BlocProvider( - create: (context) => HetznerMetricsCubit()..restart(), + create: (final context) => HetznerMetricsCubit()..restart(), child: _Chart(), ), - SizedBox(height: 20), + const SizedBox(height: 20), _TextDetails(), ], ), diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart index c4d6ed02..18d425e6 100644 --- a/lib/ui/pages/server_details/server_settings.dart +++ b/lib/ui/pages/server_details/server_settings.dart @@ -1,66 +1,66 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _ServerSettings extends StatelessWidget { const _ServerSettings({ - Key? key, required this.tabController, - }) : super(key: key); + }); final TabController tabController; @override - Widget build(BuildContext context) { - var serverDetailsState = context.watch().state; + Widget build(final BuildContext context) { + final serverDetailsState = context.watch().state; if (serverDetailsState is ServerDetailsNotReady) { - return Text('not ready'); + return const Text('not ready'); } else if (serverDetailsState is! Loaded) { return BrandLoader.horizontal(); } return ListView( padding: paddingH15V0, children: [ - SizedBox(height: 10), + const SizedBox(height: 10), Container( height: 52, alignment: Alignment.centerLeft, - padding: EdgeInsets.only(left: 1), - child: Container( - child: Row( - children: [ - IconButton( - icon: Icon(BrandIcons.arrow_left), - onPressed: () => tabController.animateTo(0), - ), - SizedBox(width: 10), - BrandText.h4('basis.settings'.tr()), - ], - ), + padding: const EdgeInsets.only(left: 1), + child: Row( + children: [ + IconButton( + icon: const Icon(BrandIcons.arrowLeft), + onPressed: () => tabController.animateTo(0), + ), + const SizedBox(width: 10), + BrandText.h4('basis.settings'.tr()), + ], ), ), - BrandDivider(), + const BrandDivider(), SwitcherBlock( - onChange: (_) {}, - child: _TextColumn( + onChange: (final _) {}, + isActive: serverDetailsState.autoUpgradeSettings.enable, + child: const _TextColumn( title: 'Allow Auto-upgrade', value: 'Wether to allow automatic packages upgrades', + hasWarning: false, ), - isActive: serverDetailsState.autoUpgradeSettings.enable, ), SwitcherBlock( - onChange: (_) {}, - child: _TextColumn( + onChange: (final _) {}, + isActive: serverDetailsState.autoUpgradeSettings.allowReboot, + child: const _TextColumn( title: 'Reboot after upgrade', value: 'Reboot without prompt after applying updates', + hasWarning: false, ), - isActive: serverDetailsState.autoUpgradeSettings.allowReboot, ), _Button( onTap: () { - Navigator.of(context).push(materialRoute(SelectTimezone())); + Navigator.of(context).push(materialRoute(const SelectTimezone())); }, child: _TextColumn( title: 'Server Timezone', value: serverDetailsState.serverTimezone.timezone.name, + hasWarning: false, ), ), ], @@ -70,60 +70,55 @@ class _ServerSettings extends StatelessWidget { class _Button extends StatelessWidget { const _Button({ - Key? key, required this.onTap, required this.child, - }) : super(key: key); + }); final Widget child; final VoidCallback onTap; @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - padding: EdgeInsets.only(top: 20, bottom: 5), - decoration: BoxDecoration( + Widget build(final BuildContext context) => InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.only(top: 20, bottom: 5), + decoration: const BoxDecoration( border: Border( - bottom: BorderSide(width: 1, color: BrandColors.dividerColor), - )), - child: child, - ), - ); - } + bottom: BorderSide(width: 1, color: BrandColors.dividerColor), + ), + ), + child: child, + ), + ); } class _TextColumn extends StatelessWidget { const _TextColumn({ - Key? key, required this.title, required this.value, this.hasWarning = false, - }) : super(key: key); + }); final String title; final String value; final bool hasWarning; @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BrandText.body1( - title, - style: TextStyle(color: hasWarning ? BrandColors.warning : null), - ), - SizedBox(height: 5), - BrandText.body1( - value, - style: TextStyle( - fontSize: 13, - height: 1.53, - color: hasWarning ? BrandColors.warning : BrandColors.gray1, + Widget build(final BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BrandText.body1( + title, + style: TextStyle(color: hasWarning ? BrandColors.warning : null), ), - ), - ], - ); - } + const SizedBox(height: 5), + BrandText.body1( + value, + style: TextStyle( + fontSize: 13, + height: 1.53, + color: hasWarning ? BrandColors.warning : BrandColors.gray1, + ), + ), + ], + ); } diff --git a/lib/ui/pages/server_details/text_details.dart b/lib/ui/pages/server_details/text_details.dart index a4620f0e..2285d305 100644 --- a/lib/ui/pages/server_details/text_details.dart +++ b/lib/ui/pages/server_details/text_details.dart @@ -1,25 +1,23 @@ -part of 'server_details.dart'; +part of 'server_details_screen.dart'; class _TextDetails extends StatelessWidget { - const _TextDetails({Key? key}) : super(key: key); - @override - Widget build(BuildContext context) { - var details = context.watch().state; + Widget build(final BuildContext context) { + final details = context.watch().state; if (details is ServerDetailsLoading || details is ServerDetailsInitial) { return _TempMessage(message: 'basis.loading'.tr()); } else if (details is ServerDetailsNotReady) { return _TempMessage(message: 'basis.no_data'.tr()); } else if (details is Loaded) { - var data = details.serverInfo; - var checkTime = details.checkTime; + final data = details.serverInfo; + final checkTime = details.checkTime; return Column( children: [ Center(child: BrandText.h3('providers.server.bottom_sheet.2'.tr())), - SizedBox(height: 10), + const SizedBox(height: 10), Table( - columnWidths: { + columnWidths: const { 0: FractionColumnWidth(.5), 1: FractionColumnWidth(.5), }, @@ -41,7 +39,7 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Status:'), getRowValue( - '${data.status.toString().split('.')[1].toUpperCase()}', + data.status.toString().split('.')[1].toUpperCase(), isBold: true, ), ], @@ -74,7 +72,7 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Price monthly:'), getRowValue( - '${data.serverType.prices[1].monthly.toString()}', + data.serverType.prices[1].monthly.toString(), ), ], ), @@ -82,17 +80,17 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Price hourly:'), getRowValue( - '${data.serverType.prices[1].hourly.toString()}', + data.serverType.prices[1].hourly.toString(), ), ], ), ], ), - SizedBox(height: 30), + const SizedBox(height: 30), Center(child: BrandText.h3('providers.server.bottom_sheet.3'.tr())), - SizedBox(height: 10), + const SizedBox(height: 10), Table( - columnWidths: { + columnWidths: const { 0: FractionColumnWidth(.5), 1: FractionColumnWidth(.5), }, @@ -102,7 +100,7 @@ class _TextDetails extends StatelessWidget { children: [ getRowTitle('Country:'), getRowValue( - '${data.location.country}', + data.location.country, ), ], ), @@ -120,7 +118,7 @@ class _TextDetails extends StatelessWidget { ), ], ), - SizedBox(height: 20), + const SizedBox(height: 20), ], ); } else { @@ -128,44 +126,38 @@ class _TextDetails extends StatelessWidget { } } - Widget getRowTitle(String title) { - return Padding( - padding: const EdgeInsets.only(right: 10), - child: BrandText.h5( - title, - textAlign: TextAlign.right, - ), - ); - } + Widget getRowTitle(final String title) => Padding( + padding: const EdgeInsets.only(right: 10), + child: BrandText.h5( + title, + textAlign: TextAlign.right, + ), + ); - Widget getRowValue(String title, {bool isBold = false}) { - return BrandText.body1( - title, - style: isBold - ? TextStyle( - fontWeight: NamedFontWeight.demiBold, - ) - : null, - ); - } + Widget getRowValue(final String title, {final bool isBold = false}) => + BrandText.body1( + title, + style: isBold + ? const TextStyle( + fontWeight: NamedFontWeight.demiBold, + ) + : null, + ); } class _TempMessage extends StatelessWidget { const _TempMessage({ - Key? key, required this.message, - }) : super(key: key); + }); final String message; @override - Widget build(BuildContext context) { - return SizedBox( - height: MediaQuery.of(context).size.height - 100, - child: Center( - child: BrandText.body2(message), - ), - ); - } + Widget build(final BuildContext context) => SizedBox( + height: MediaQuery.of(context).size.height - 100, + child: Center( + child: BrandText.body2(message), + ), + ); } final DateFormat formatter = DateFormat('HH:mm:ss'); diff --git a/lib/ui/pages/server_details/time_zone/lang.dart b/lib/ui/pages/server_details/time_zone/lang.dart index 4ea55019..b46fa721 100644 --- a/lib/ui/pages/server_details/time_zone/lang.dart +++ b/lib/ui/pages/server_details/time_zone/lang.dart @@ -1,431 +1,431 @@ final russian = { - "Pacific/Midway": "Мидуэй", - "Pacific/Niue": "Ниуэ", - "Pacific/Pago_Pago": "Паго-Паго", - "America/Adak": "Адак", - "Pacific/Honolulu": "Гонолулу", - "Pacific/Johnston": "Джонстон", - "Pacific/Rarotonga": "Раротонга", - "Pacific/Tahiti": "Таити", - "US/Hawaii": "Гавайи", - "Pacific/Marquesas": "Маркизские острова", - "America/Sitka": "Ситка", - "America/Anchorage": "Анкоридж", - "America/Metlakatla": "Метлакатла", - "America/Juneau": "Джуно", - "US/Alaska": "Аляска", - "America/Nome": "Ном", - "America/Yakutat": "Якутат", - "Pacific/Gambier": "Гамбье", - "America/Tijuana": "Тихуана", - "Pacific/Pitcairn": "Питкэрн", - "US/Pacific": "США/Тихий океан", - "Canada/Pacific": "США/Тихий океан", - "America/Los_Angeles": "Лос-Анджелес", - "America/Vancouver": "Ванкувер", - "America/Santa_Isabel": "Санта-Изабель", - "America/Chihuahua": "Чихуахуа", - "America/Cambridge_Bay": "Кембридж-Бэй", - "America/Inuvik": "Инувик", - "America/Boise": "Бойсе", - "America/Dawson": "Доусон", - "America/Mazatlan": "Масатлан", - "America/Dawson_Creek": "Доусон-Крик", - "US/Arizona": "Аризона", - "America/Denver": "Денвер", - "US/Mountain": "гора", - "America/Edmonton": "Эдмонтон", - "America/Yellowknife": "Йеллоунайф", - "America/Ojinaga": "Охинага", - "America/Phoenix": "Феникс", - "America/Whitehorse": "Белая лошадь", - "Canada/Mountain": "гора", - "America/Hermosillo": "Эрмосильо", - "America/Creston": "Крестон", - "America/Swift_Current": "Свифт Керрент", - "America/Tegucigalpa": "Тегусигальпа", - "America/Regina": "Регина", - "America/Rankin_Inlet": "Ранкин-Инлет", - "America/Rainy_River": "Райни-Ривер", - "America/Winnipeg": "Виннипег", - "America/North_Dakota/Center": "Северная Дакота/Центр", - "America/North_Dakota/Beulah": "Северная Дакота/Беула", - "America/Monterrey": "Монтеррей", - "America/Mexico_City": "Мехико", - "US/Central": "Центральный", - "America/Merida": "Мерида", - "America/Menominee": "Меномини", - "America/Matamoros": "Матаморос", - "America/Managua": "Манагуа", - "America/North_Dakota/New_Salem": "Северная Дакота/Нью-Салем", - "Pacific/Galapagos": "Галапагосские острова", - "America/Indiana/Tell_City": "Индиана/Телл-Сити", - "America/Indiana/Knox": "Индиана/Нокс", - "Canada/Central": "Центральный", - "America/Guatemala": "Гватемала", - "America/El_Salvador": "Сальвадор", - "America/Costa_Rica": "Коста-Рика", - "America/Chicago": "Чикаго", - "America/Belize": "Белиз", - "America/Bahia_Banderas": "Баия де Бандерас", - "America/Resolute": "Резольют", - "America/Atikokan": "Атикокан", - "America/Lima": "Лима", - "America/Bogota": "Богота", - "America/Cancun": "Канкун", - "America/Cayman": "Кайман", - "America/Detroit": "Детройт", - "America/Indiana/Indianapolis": "Индиана/Индианаполис", - "America/Eirunepe": "Эйрунепе", - "America/Grand_Turk": "Гранд-Терк", - "America/Guayaquil": "Гуаякиль", - "America/Havana": "Гавана", - "America/Indiana/Marengo": "Индиана/Маренго", - "America/Indiana/Petersburg": "Индиана/Петербург", - "America/Indiana/Vevay": "Индиана/Вева", - "America/Indiana/Vincennes": "Индиана/Винсеннес", - "America/Indiana/Winamac": "Индиана/Винамак", - "America/Iqaluit": "Икалуит", - "America/Jamaica": "Ямайка", - "America/Kentucky/Louisville": "Кентукки/Луисвилл", - "America/Nassau": "Нассау", - "America/Toronto": "Торонто", - "America/Montreal": "Монреаль", - "America/Pangnirtung": "Пангниртунг", - "America/Port-au-Prince": "Порт-о-Пренс", - "America/Kentucky/Monticello": "Кентукки/Монтичелло", - "Canada/Eastern": "Канада/Восточное", - "US/Eastern": "США/Восточное", - "America/Thunder_Bay": "Тандер-Бей", - "Pacific/Easter": "Пасха", - "America/Panama": "Панама", - "America/Nipigon": "Нипигон", - "America/Rio_Branco": "Рио-Бранко", - "America/New_York": "Нью-Йорк", - "Canada/Atlantic": "Атлантика", - "America/Kralendijk": "Кралендейк", - "America/La_Paz": "Ла-Пас", - "America/Halifax": "Галифакс", - "America/Lower_Princes": "Лоуэр-Принс-Куотер", - "America/Manaus": "Манаус", - "America/Marigot": "Мариго", - "America/Martinique": "Мартиника", - "America/Moncton": "Монктон", - "America/Guyana": "Гайана", - "America/Montserrat": "Монтсеррат", - "America/Guadeloupe": "Гваделупа", - "America/Grenada": "Гренада", - "America/Goose_Bay": "Гуз-Бей", - "America/Glace_Bay": "Глас Бэй", - "America/Curacao": "Кюрасао", - "America/Cuiaba": "Куяба", - "America/Port_of_Spain": "Порт-оф-Спейн", - "America/Porto_Velho": "Порту-Велью", - "America/Puerto_Rico": "Пуэрто-Рико", - "America/Caracas": "Каракас", - "America/Santo_Domingo": "Санто-Доминго", - "America/St_Barthelemy": "Святой Бартелеми", - "Atlantic/Bermuda": "Бермуды", - "America/St_Kitts": "Сент-Китс", - "America/St_Lucia": "Святая Люсия", - "America/St_Thomas": "Сент-Томас", - "America/St_Vincent": "Сент-Винсент", - "America/Thule": "Туле", - "America/Campo_Grande": "Кампу-Гранди", - "America/Boa_Vista": "Боа-Виста", - "America/Tortola": "Тортола", - "America/Aruba": "Аруба", - "America/Blanc-Sablon": "Блан-Саблон", - "America/Barbados": "Барбадос", - "America/Anguilla": "Ангилья", - "America/Antigua": "Антигуа", - "America/Dominica": "Доминика", - "Canada/Newfoundland": "Ньюфаундленд", - "America/St_Johns": "Сент-Джонс", - "America/Sao_Paulo": "Сан-Паулу", - "Atlantic/Stanley": "Стэнли", - "America/Miquelon": "Микелон", - "America/Argentina/Salta": "Аргентина/Сальта", - "America/Montevideo": "Монтевидео", - "America/Argentina/Rio_Gallegos": "Аргентина/Рио-Гальегос", - "America/Argentina/Mendoza": "Аргентина/Мендоса", - "America/Argentina/La_Rioja": "Аргентина/Ла-Риоха", - "America/Argentina/Jujuy": "Аргентина/Жужуй", - "Antarctica/Rothera": "Ротера", - "America/Argentina/Cordoba": "Аргентина/Кордова", - "America/Argentina/Catamarca": "Аргентина/Катамарка", - "America/Argentina/Ushuaia": "Аргентина/Ушуая", - "America/Argentina/Tucuman": "Аргентина/Тукуман", - "America/Paramaribo": "Парамарибо", - "America/Argentina/San_Luis": "Аргентина/Сан-Луис", - "America/Recife": "Ресифи", - "America/Argentina/Buenos_Aires": "Аргентина/Буэнос-Айрес", - "America/Asuncion": "Асунсьон", - "America/Maceio": "Масейо", - "America/Santarem": "Сантарен", - "America/Santiago": "Сантьяго", - "Antarctica/Palmer": "Палмер", - "America/Argentina/San_Juan": "Аргентина/Сан-Хуан", - "America/Fortaleza": "Форталеза", - "America/Cayenne": "Кайенна", - "America/Godthab": "Годтаб", - "America/Belem": "Белен", - "America/Araguaina": "Арагуайна", - "America/Bahia": "Баия", - "Atlantic/South_Georgia": "Южная_Грузия", - "America/Noronha": "Норонья", - "Atlantic/Azores": "Азорские острова", - "Atlantic/Cape_Verde": "Кабо-Верде", - "America/Scoresbysund": "Скорсбисунд", - "Africa/Accra": "Аккра", - "Atlantic/Faroe": "Фарерские острова", - "Europe/Guernsey": "Гернси", - "Africa/Dakar": "Дакар", - "Europe/Isle_of_Man": "Остров Мэн", - "Africa/Conakry": "Конакри", - "Africa/Abidjan": "Абиджан", - "Atlantic/Canary": "канарейка", - "Africa/Banjul": "Банжул", - "Europe/Jersey": "Джерси", - "Atlantic/St_Helena": "Остров Святой Елены", - "Africa/Bissau": "Бисау", - "Europe/London": "Лондон", - "Africa/Nouakchott": "Нуакшот", - "Africa/Lome": "Ломе", - "America/Danmarkshavn": "Данмарксхавн", - "Africa/Ouagadougou": "Уагадугу", - "Europe/Lisbon": "Лиссабон", - "Africa/Sao_Tome": "Сан-Томе", - "Africa/Monrovia": "Монровия", - "Atlantic/Reykjavik": "Рейкьявик", - "Antarctica/Troll": "Тролль", - "Atlantic/Madeira": "Мадейра", - "Africa/Bamako": "Бамако", - "Europe/Dublin": "Дублин", - "Africa/Freetown": "Фритаун", - "Europe/Monaco": "Монако", - "Europe/Skopje": "Скопье", - "Europe/Amsterdam": "Амстердам", - "Africa/Tunis": "Тунис", - "Arctic/Longyearbyen": "Лонгйир", - "Africa/Bangui": "Банги", - "Africa/Lagos": "Лагос", - "Africa/Douala": "Дуала", - "Africa/Libreville": "Либревиль", - "Europe/Belgrade": "Белград", - "Europe/Stockholm": "Стокгольм", - "Europe/Berlin": "Берлин", - "Europe/Zurich": "Цюрих", - "Europe/Zagreb": "Загреб", - "Europe/Warsaw": "Варшава", - "Africa/Luanda": "Луанда", - "Africa/Porto-Novo": "Порто-Ново", - "Africa/Brazzaville": "Браззавиль", - "Europe/Vienna": "Вена", - "Europe/Vatican": "Ватикан", - "Europe/Vaduz": "Вадуц", - "Europe/Tirane": "Тиран", - "Europe/Bratislava": "Братислава", - "Europe/Brussels": "Брюссель", - "Europe/Paris": "Париж", - "Europe/Sarajevo": "Сараево", - "Europe/San_Marino": "Сан-Марино", - "Europe/Rome": "Рим", - "Africa/El_Aaiun": "Эль-Аайун", - "Africa/Casablanca": "Касабланка", - "Europe/Malta": "Мальта", - "Africa/Ceuta": "Сеута", - "Europe/Gibraltar": "Гибралтар", - "Africa/Malabo": "Малабо", - "Europe/Busingen": "Бузинген", - "Africa/Ndjamena": "Нджамена", - "Europe/Andorra": "Андорра", - "Europe/Oslo": "Осло", - "Europe/Luxembourg": "Люксембург", - "Africa/Niamey": "Ниамей", - "Europe/Copenhagen": "Копенгаген", - "Europe/Madrid": "Мадрид", - "Europe/Budapest": "Будапешт", - "Africa/Algiers": "Алжир", - "Europe/Ljubljana": "Любляна", - "Europe/Podgorica": "Подгорица", - "Africa/Kinshasa": "Киншаса", - "Europe/Prague": "Прага", - "Europe/Riga": "Рига", - "Africa/Bujumbura": "Бужумбура", - "Africa/Lubumbashi": "Лубумбаши", - "Europe/Bucharest": "Бухарест", - "Africa/Blantyre": "Блантайр", - "Asia/Nicosia": "Никосия", - "Europe/Sofia": "София", - "Asia/Jerusalem": "Иерусалим", - "Europe/Tallinn": "Таллинн", - "Europe/Uzhgorod": "Ужгород", - "Africa/Lusaka": "Лусака", - "Europe/Mariehamn": "Мариехамн", - "Asia/Hebron": "Хеврон", - "Asia/Gaza": "Газа", - "Asia/Damascus": "Дамаск", - "Europe/Zaporozhye": "Запорожье", - "Asia/Beirut": "Бейрут", - "Africa/Juba": "Джуба", - "Africa/Harare": "Хараре", - "Europe/Athens": "Афины", - "Europe/Kiev": "Киев", - "Europe/Kaliningrad": "Калининград", - "Africa/Khartoum": "Хартум", - "Africa/Cairo": "Каир", - "Africa/Kigali": "Кигали", - "Asia/Amman": "Амман", - "Africa/Maputo": "Мапуту", - "Africa/Gaborone": "Габороне", - "Africa/Tripoli": "Триполи", - "Africa/Maseru": "Масеру", - "Africa/Windhoek": "Виндхук", - "Africa/Johannesburg": "Йоханнесбург", - "Europe/Chisinau": "Кишинев", - "Africa/Mbabane": "Мбабане", - "Europe/Vilnius": "Вильнюс", - "Europe/Helsinki": "Хельсинки", - "Europe/Moscow": "Москва", - "Africa/Kampala": "Кампала", - "Africa/Nairobi": "Найроби", - "Africa/Asmara": "Асмэра", - "Europe/Istanbul": "Стамбул", - "Asia/Riyadh": "Эр-Рияд", - "Asia/Qatar": "Катар", - "Europe/Minsk": "Минск", - "Indian/Comoro": "Коморо", - "Asia/Kuwait": "Кувейт", - "Africa/Addis_Ababa": "Аддис-Абеба", - "Africa/Dar_es_Salaam": "Дар-эс-Салам", - "Europe/Volgograd": "Волгоград", - "Indian/Antananarivo": "Антананариву", - "Asia/Bahrain": "Бахрейн", - "Asia/Baghdad": "Багдад", - "Indian/Mayotte": "Майотта", - "Africa/Djibouti": "Джибути", - "Europe/Simferopol": "Симферополь", - "Asia/Aden": "Аден", - "Antarctica/Syowa": "Сёва", - "Africa/Mogadishu": "Могадишо", - "Asia/Tehran": "Тегеран", - "Asia/Yerevan": "Ереван", - "Asia/Tbilisi": "Тбилиси", - "Asia/Muscat": "Мускат", - "Europe/Samara": "Самара", - "Indian/Mahe": "Маэ", - "Asia/Baku": "Баку", - "Indian/Mauritius": "Маврикий", - "Indian/Reunion": "Воссоединение", - "Asia/Dubai": "Дубай", - "Asia/Kabul": "Кабул", - "Asia/Ashgabat": "Ашхабад", - "Antarctica/Mawson": "Моусон", - "Asia/Aqtau": "Актау", - "Asia/Yekaterinburg": "Екатеринбург", - "Asia/Aqtobe": "Актобе", - "Asia/Dushanbe": "Душанбе", - "Asia/Tashkent": "Ташкент", - "Asia/Samarkand": "Самарканд", - "Asia/Qyzylorda": "Кызылорда", - "Asia/Oral": "Оральный", - "Asia/Karachi": "Карачи", - "Indian/Kerguelen": "Кергелен", - "Indian/Maldives": "Мальдивы", - "Asia/Kolkata": "Калькутта", - "Asia/Colombo": "Коломбо", - "Asia/Kathmandu": "Катманду", - "Antarctica/Vostok": "Восток", - "Asia/Almaty": "Алматы", - "Asia/Urumqi": "Урумчи", - "Asia/Thimphu": "Тхимпху", - "Asia/Omsk": "Омск", - "Asia/Dhaka": "Дакка", - "Indian/Chagos": "Чагос", - "Asia/Bishkek": "Бишкек", - "Asia/Rangoon": "Рангун", - "Indian/Cocos": "кокосы", - "Asia/Bangkok": "Бангкок", - "Asia/Hovd": "Ховд", - "Asia/Novokuznetsk": "Новокузнецк", - "Asia/Vientiane": "Вьентьян", - "Asia/Krasnoyarsk": "Красноярск", - "Antarctica/Davis": "Дэвис", - "Asia/Novosibirsk": "Новосибирск", - "Asia/Phnom_Penh": "Пномпень", - "Asia/Pontianak": "Понтианак", - "Asia/Jakarta": "Джакарта", - "Asia/Ho_Chi_Minh": "Хо Ши Мин", - "Indian/Christmas": "Рождество", - "Asia/Manila": "Манила", - "Asia/Makassar": "Макассар", - "Asia/Macau": "Макао", - "Asia/Kuala_Lumpur": "Куала-Лумпур", - "Asia/Singapore": "Сингапур", - "Asia/Shanghai": "Шанхай", - "Asia/Irkutsk": "Иркутск", - "Asia/Kuching": "Кучинг", - "Asia/Hong_Kong": "Гонконг", - "Australia/Perth": "Перт", - "Asia/Taipei": "Тайбэй", - "Asia/Brunei": "Бруней", - "Asia/Choibalsan": "Чойбалсан", - "Asia/Ulaanbaatar": "Улан-Батор", - "Australia/Eucla": "Евкла", - "Asia/Yakutsk": "Якутск", - "Asia/Dili": "Дили", - "Pacific/Palau": "Палау", - "Asia/Jayapura": "Джаяпура", - "Asia/Seoul": "Сеул", - "Asia/Pyongyang": "Пхеньян", - "Asia/Khandyga": "Хандыга", - "Asia/Chita": "Чита", - "Asia/Tokyo": "Токио", - "Australia/Darwin": "Дарвин", - "Pacific/Saipan": "Сайпан", - "Australia/Brisbane": "Брисбен", - "Pacific/Port_Moresby": "Порт-Морсби", - "Pacific/Chuuk": "Чуук", - "Antarctica/DumontDUrville": "Дюмон-д'Юрвиль", - "Pacific/Guam": "Гуам", - "Australia/Lindeman": "Линдеман", - "Asia/Ust-Nera": "Усть-Нера", - "Asia/Vladivostok": "Владивосток", - "Australia/Broken_Hill": "Брокен-Хилл", - "Australia/Adelaide": "Аделаида", - "Asia/Sakhalin": "Сахалин", - "Pacific/Guadalcanal": "Гуадалканал", - "Pacific/Efate": "Эфате", - "Antarctica/Casey": "Кейси", - "Antarctica/Macquarie": "Маккуори", - "Pacific/Kosrae": "Косрае", - "Australia/Sydney": "Сидней", - "Pacific/Noumea": "Нумеа", - "Australia/Melbourne": "Мельбурн", - "Australia/Lord_Howe": "Остров Лорд-Хау", - "Australia/Hobart": "Хобарт", - "Pacific/Pohnpei": "Понпеи", - "Australia/Currie": "Карри", - "Asia/Srednekolymsk": "Среднеколымск", - "Asia/Magadan": "Магадан", - "Pacific/Kwajalein": "Кваджалейн", - "Pacific/Majuro": "Маджуро", - "Pacific/Funafuti": "Фунафути", - "Asia/Anadyr": "Анадырь", - "Pacific/Nauru": "Науру", - "Asia/Kamchatka": "Камчатка", - "Pacific/Fiji": "Фиджи", - "Pacific/Norfolk": "Норфолк", - "Pacific/Tarawa": "Тарава", - "Pacific/Wallis": "Уоллис", - "Pacific/Wake": "Будить", - "Pacific/Tongatapu": "Тонгатапу", - "Antarctica/McMurdo": "МакМердо", - "Pacific/Enderbury": "Эндербери", - "Pacific/Fakaofo": "Факаофо", - "Pacific/Auckland": "Окленд", - "Pacific/Chatham": "Чатем", - "Pacific/Kiritimati": "Киритимати", - "Pacific/Apia": "Апиа", + 'Pacific/Midway': 'Мидуэй', + 'Pacific/Niue': 'Ниуэ', + 'Pacific/Pago_Pago': 'Паго-Паго', + 'America/Adak': 'Адак', + 'Pacific/Honolulu': 'Гонолулу', + 'Pacific/Johnston': 'Джонстон', + 'Pacific/Rarotonga': 'Раротонга', + 'Pacific/Tahiti': 'Таити', + 'US/Hawaii': 'Гавайи', + 'Pacific/Marquesas': 'Маркизские острова', + 'America/Sitka': 'Ситка', + 'America/Anchorage': 'Анкоридж', + 'America/Metlakatla': 'Метлакатла', + 'America/Juneau': 'Джуно', + 'US/Alaska': 'Аляска', + 'America/Nome': 'Ном', + 'America/Yakutat': 'Якутат', + 'Pacific/Gambier': 'Гамбье', + 'America/Tijuana': 'Тихуана', + 'Pacific/Pitcairn': 'Питкэрн', + 'US/Pacific': 'США/Тихий океан', + 'Canada/Pacific': 'США/Тихий океан', + 'America/Los_Angeles': 'Лос-Анджелес', + 'America/Vancouver': 'Ванкувер', + 'America/Santa_Isabel': 'Санта-Изабель', + 'America/Chihuahua': 'Чихуахуа', + 'America/Cambridge_Bay': 'Кембридж-Бэй', + 'America/Inuvik': 'Инувик', + 'America/Boise': 'Бойсе', + 'America/Dawson': 'Доусон', + 'America/Mazatlan': 'Масатлан', + 'America/Dawson_Creek': 'Доусон-Крик', + 'US/Arizona': 'Аризона', + 'America/Denver': 'Денвер', + 'US/Mountain': 'гора', + 'America/Edmonton': 'Эдмонтон', + 'America/Yellowknife': 'Йеллоунайф', + 'America/Ojinaga': 'Охинага', + 'America/Phoenix': 'Феникс', + 'America/Whitehorse': 'Белая лошадь', + 'Canada/Mountain': 'гора', + 'America/Hermosillo': 'Эрмосильо', + 'America/Creston': 'Крестон', + 'America/Swift_Current': 'Свифт Керрент', + 'America/Tegucigalpa': 'Тегусигальпа', + 'America/Regina': 'Регина', + 'America/Rankin_Inlet': 'Ранкин-Инлет', + 'America/Rainy_River': 'Райни-Ривер', + 'America/Winnipeg': 'Виннипег', + 'America/North_Dakota/Center': 'Северная Дакота/Центр', + 'America/North_Dakota/Beulah': 'Северная Дакота/Беула', + 'America/Monterrey': 'Монтеррей', + 'America/Mexico_City': 'Мехико', + 'US/Central': 'Центральный', + 'America/Merida': 'Мерида', + 'America/Menominee': 'Меномини', + 'America/Matamoros': 'Матаморос', + 'America/Managua': 'Манагуа', + 'America/North_Dakota/New_Salem': 'Северная Дакота/Нью-Салем', + 'Pacific/Galapagos': 'Галапагосские острова', + 'America/Indiana/Tell_City': 'Индиана/Телл-Сити', + 'America/Indiana/Knox': 'Индиана/Нокс', + 'Canada/Central': 'Центральный', + 'America/Guatemala': 'Гватемала', + 'America/El_Salvador': 'Сальвадор', + 'America/Costa_Rica': 'Коста-Рика', + 'America/Chicago': 'Чикаго', + 'America/Belize': 'Белиз', + 'America/Bahia_Banderas': 'Баия де Бандерас', + 'America/Resolute': 'Резольют', + 'America/Atikokan': 'Атикокан', + 'America/Lima': 'Лима', + 'America/Bogota': 'Богота', + 'America/Cancun': 'Канкун', + 'America/Cayman': 'Кайман', + 'America/Detroit': 'Детройт', + 'America/Indiana/Indianapolis': 'Индиана/Индианаполис', + 'America/Eirunepe': 'Эйрунепе', + 'America/Grand_Turk': 'Гранд-Терк', + 'America/Guayaquil': 'Гуаякиль', + 'America/Havana': 'Гавана', + 'America/Indiana/Marengo': 'Индиана/Маренго', + 'America/Indiana/Petersburg': 'Индиана/Петербург', + 'America/Indiana/Vevay': 'Индиана/Вева', + 'America/Indiana/Vincennes': 'Индиана/Винсеннес', + 'America/Indiana/Winamac': 'Индиана/Винамак', + 'America/Iqaluit': 'Икалуит', + 'America/Jamaica': 'Ямайка', + 'America/Kentucky/Louisville': 'Кентукки/Луисвилл', + 'America/Nassau': 'Нассау', + 'America/Toronto': 'Торонто', + 'America/Montreal': 'Монреаль', + 'America/Pangnirtung': 'Пангниртунг', + 'America/Port-au-Prince': 'Порт-о-Пренс', + 'America/Kentucky/Monticello': 'Кентукки/Монтичелло', + 'Canada/Eastern': 'Канада/Восточное', + 'US/Eastern': 'США/Восточное', + 'America/Thunder_Bay': 'Тандер-Бей', + 'Pacific/Easter': 'Пасха', + 'America/Panama': 'Панама', + 'America/Nipigon': 'Нипигон', + 'America/Rio_Branco': 'Рио-Бранко', + 'America/New_York': 'Нью-Йорк', + 'Canada/Atlantic': 'Атлантика', + 'America/Kralendijk': 'Кралендейк', + 'America/La_Paz': 'Ла-Пас', + 'America/Halifax': 'Галифакс', + 'America/Lower_Princes': 'Лоуэр-Принс-Куотер', + 'America/Manaus': 'Манаус', + 'America/Marigot': 'Мариго', + 'America/Martinique': 'Мартиника', + 'America/Moncton': 'Монктон', + 'America/Guyana': 'Гайана', + 'America/Montserrat': 'Монтсеррат', + 'America/Guadeloupe': 'Гваделупа', + 'America/Grenada': 'Гренада', + 'America/Goose_Bay': 'Гуз-Бей', + 'America/Glace_Bay': 'Глас Бэй', + 'America/Curacao': 'Кюрасао', + 'America/Cuiaba': 'Куяба', + 'America/Port_of_Spain': 'Порт-оф-Спейн', + 'America/Porto_Velho': 'Порту-Велью', + 'America/Puerto_Rico': 'Пуэрто-Рико', + 'America/Caracas': 'Каракас', + 'America/Santo_Domingo': 'Санто-Доминго', + 'America/St_Barthelemy': 'Святой Бартелеми', + 'Atlantic/Bermuda': 'Бермуды', + 'America/St_Kitts': 'Сент-Китс', + 'America/St_Lucia': 'Святая Люсия', + 'America/St_Thomas': 'Сент-Томас', + 'America/St_Vincent': 'Сент-Винсент', + 'America/Thule': 'Туле', + 'America/Campo_Grande': 'Кампу-Гранди', + 'America/Boa_Vista': 'Боа-Виста', + 'America/Tortola': 'Тортола', + 'America/Aruba': 'Аруба', + 'America/Blanc-Sablon': 'Блан-Саблон', + 'America/Barbados': 'Барбадос', + 'America/Anguilla': 'Ангилья', + 'America/Antigua': 'Антигуа', + 'America/Dominica': 'Доминика', + 'Canada/Newfoundland': 'Ньюфаундленд', + 'America/St_Johns': 'Сент-Джонс', + 'America/Sao_Paulo': 'Сан-Паулу', + 'Atlantic/Stanley': 'Стэнли', + 'America/Miquelon': 'Микелон', + 'America/Argentina/Salta': 'Аргентина/Сальта', + 'America/Montevideo': 'Монтевидео', + 'America/Argentina/Rio_Gallegos': 'Аргентина/Рио-Гальегос', + 'America/Argentina/Mendoza': 'Аргентина/Мендоса', + 'America/Argentina/La_Rioja': 'Аргентина/Ла-Риоха', + 'America/Argentina/Jujuy': 'Аргентина/Жужуй', + 'Antarctica/Rothera': 'Ротера', + 'America/Argentina/Cordoba': 'Аргентина/Кордова', + 'America/Argentina/Catamarca': 'Аргентина/Катамарка', + 'America/Argentina/Ushuaia': 'Аргентина/Ушуая', + 'America/Argentina/Tucuman': 'Аргентина/Тукуман', + 'America/Paramaribo': 'Парамарибо', + 'America/Argentina/San_Luis': 'Аргентина/Сан-Луис', + 'America/Recife': 'Ресифи', + 'America/Argentina/Buenos_Aires': 'Аргентина/Буэнос-Айрес', + 'America/Asuncion': 'Асунсьон', + 'America/Maceio': 'Масейо', + 'America/Santarem': 'Сантарен', + 'America/Santiago': 'Сантьяго', + 'Antarctica/Palmer': 'Палмер', + 'America/Argentina/San_Juan': 'Аргентина/Сан-Хуан', + 'America/Fortaleza': 'Форталеза', + 'America/Cayenne': 'Кайенна', + 'America/Godthab': 'Годтаб', + 'America/Belem': 'Белен', + 'America/Araguaina': 'Арагуайна', + 'America/Bahia': 'Баия', + 'Atlantic/South_Georgia': 'Южная_Грузия', + 'America/Noronha': 'Норонья', + 'Atlantic/Azores': 'Азорские острова', + 'Atlantic/Cape_Verde': 'Кабо-Верде', + 'America/Scoresbysund': 'Скорсбисунд', + 'Africa/Accra': 'Аккра', + 'Atlantic/Faroe': 'Фарерские острова', + 'Europe/Guernsey': 'Гернси', + 'Africa/Dakar': 'Дакар', + 'Europe/Isle_of_Man': 'Остров Мэн', + 'Africa/Conakry': 'Конакри', + 'Africa/Abidjan': 'Абиджан', + 'Atlantic/Canary': 'канарейка', + 'Africa/Banjul': 'Банжул', + 'Europe/Jersey': 'Джерси', + 'Atlantic/St_Helena': 'Остров Святой Елены', + 'Africa/Bissau': 'Бисау', + 'Europe/London': 'Лондон', + 'Africa/Nouakchott': 'Нуакшот', + 'Africa/Lome': 'Ломе', + 'America/Danmarkshavn': 'Данмарксхавн', + 'Africa/Ouagadougou': 'Уагадугу', + 'Europe/Lisbon': 'Лиссабон', + 'Africa/Sao_Tome': 'Сан-Томе', + 'Africa/Monrovia': 'Монровия', + 'Atlantic/Reykjavik': 'Рейкьявик', + 'Antarctica/Troll': 'Тролль', + 'Atlantic/Madeira': 'Мадейра', + 'Africa/Bamako': 'Бамако', + 'Europe/Dublin': 'Дублин', + 'Africa/Freetown': 'Фритаун', + 'Europe/Monaco': 'Монако', + 'Europe/Skopje': 'Скопье', + 'Europe/Amsterdam': 'Амстердам', + 'Africa/Tunis': 'Тунис', + 'Arctic/Longyearbyen': 'Лонгйир', + 'Africa/Bangui': 'Банги', + 'Africa/Lagos': 'Лагос', + 'Africa/Douala': 'Дуала', + 'Africa/Libreville': 'Либревиль', + 'Europe/Belgrade': 'Белград', + 'Europe/Stockholm': 'Стокгольм', + 'Europe/Berlin': 'Берлин', + 'Europe/Zurich': 'Цюрих', + 'Europe/Zagreb': 'Загреб', + 'Europe/Warsaw': 'Варшава', + 'Africa/Luanda': 'Луанда', + 'Africa/Porto-Novo': 'Порто-Ново', + 'Africa/Brazzaville': 'Браззавиль', + 'Europe/Vienna': 'Вена', + 'Europe/Vatican': 'Ватикан', + 'Europe/Vaduz': 'Вадуц', + 'Europe/Tirane': 'Тиран', + 'Europe/Bratislava': 'Братислава', + 'Europe/Brussels': 'Брюссель', + 'Europe/Paris': 'Париж', + 'Europe/Sarajevo': 'Сараево', + 'Europe/San_Marino': 'Сан-Марино', + 'Europe/Rome': 'Рим', + 'Africa/El_Aaiun': 'Эль-Аайун', + 'Africa/Casablanca': 'Касабланка', + 'Europe/Malta': 'Мальта', + 'Africa/Ceuta': 'Сеута', + 'Europe/Gibraltar': 'Гибралтар', + 'Africa/Malabo': 'Малабо', + 'Europe/Busingen': 'Бузинген', + 'Africa/Ndjamena': 'Нджамена', + 'Europe/Andorra': 'Андорра', + 'Europe/Oslo': 'Осло', + 'Europe/Luxembourg': 'Люксембург', + 'Africa/Niamey': 'Ниамей', + 'Europe/Copenhagen': 'Копенгаген', + 'Europe/Madrid': 'Мадрид', + 'Europe/Budapest': 'Будапешт', + 'Africa/Algiers': 'Алжир', + 'Europe/Ljubljana': 'Любляна', + 'Europe/Podgorica': 'Подгорица', + 'Africa/Kinshasa': 'Киншаса', + 'Europe/Prague': 'Прага', + 'Europe/Riga': 'Рига', + 'Africa/Bujumbura': 'Бужумбура', + 'Africa/Lubumbashi': 'Лубумбаши', + 'Europe/Bucharest': 'Бухарест', + 'Africa/Blantyre': 'Блантайр', + 'Asia/Nicosia': 'Никосия', + 'Europe/Sofia': 'София', + 'Asia/Jerusalem': 'Иерусалим', + 'Europe/Tallinn': 'Таллинн', + 'Europe/Uzhgorod': 'Ужгород', + 'Africa/Lusaka': 'Лусака', + 'Europe/Mariehamn': 'Мариехамн', + 'Asia/Hebron': 'Хеврон', + 'Asia/Gaza': 'Газа', + 'Asia/Damascus': 'Дамаск', + 'Europe/Zaporozhye': 'Запорожье', + 'Asia/Beirut': 'Бейрут', + 'Africa/Juba': 'Джуба', + 'Africa/Harare': 'Хараре', + 'Europe/Athens': 'Афины', + 'Europe/Kiev': 'Киев', + 'Europe/Kaliningrad': 'Калининград', + 'Africa/Khartoum': 'Хартум', + 'Africa/Cairo': 'Каир', + 'Africa/Kigali': 'Кигали', + 'Asia/Amman': 'Амман', + 'Africa/Maputo': 'Мапуту', + 'Africa/Gaborone': 'Габороне', + 'Africa/Tripoli': 'Триполи', + 'Africa/Maseru': 'Масеру', + 'Africa/Windhoek': 'Виндхук', + 'Africa/Johannesburg': 'Йоханнесбург', + 'Europe/Chisinau': 'Кишинев', + 'Africa/Mbabane': 'Мбабане', + 'Europe/Vilnius': 'Вильнюс', + 'Europe/Helsinki': 'Хельсинки', + 'Europe/Moscow': 'Москва', + 'Africa/Kampala': 'Кампала', + 'Africa/Nairobi': 'Найроби', + 'Africa/Asmara': 'Асмэра', + 'Europe/Istanbul': 'Стамбул', + 'Asia/Riyadh': 'Эр-Рияд', + 'Asia/Qatar': 'Катар', + 'Europe/Minsk': 'Минск', + 'Indian/Comoro': 'Коморо', + 'Asia/Kuwait': 'Кувейт', + 'Africa/Addis_Ababa': 'Аддис-Абеба', + 'Africa/Dar_es_Salaam': 'Дар-эс-Салам', + 'Europe/Volgograd': 'Волгоград', + 'Indian/Antananarivo': 'Антананариву', + 'Asia/Bahrain': 'Бахрейн', + 'Asia/Baghdad': 'Багдад', + 'Indian/Mayotte': 'Майотта', + 'Africa/Djibouti': 'Джибути', + 'Europe/Simferopol': 'Симферополь', + 'Asia/Aden': 'Аден', + 'Antarctica/Syowa': 'Сёва', + 'Africa/Mogadishu': 'Могадишо', + 'Asia/Tehran': 'Тегеран', + 'Asia/Yerevan': 'Ереван', + 'Asia/Tbilisi': 'Тбилиси', + 'Asia/Muscat': 'Мускат', + 'Europe/Samara': 'Самара', + 'Indian/Mahe': 'Маэ', + 'Asia/Baku': 'Баку', + 'Indian/Mauritius': 'Маврикий', + 'Indian/Reunion': 'Воссоединение', + 'Asia/Dubai': 'Дубай', + 'Asia/Kabul': 'Кабул', + 'Asia/Ashgabat': 'Ашхабад', + 'Antarctica/Mawson': 'Моусон', + 'Asia/Aqtau': 'Актау', + 'Asia/Yekaterinburg': 'Екатеринбург', + 'Asia/Aqtobe': 'Актобе', + 'Asia/Dushanbe': 'Душанбе', + 'Asia/Tashkent': 'Ташкент', + 'Asia/Samarkand': 'Самарканд', + 'Asia/Qyzylorda': 'Кызылорда', + 'Asia/Oral': 'Оральный', + 'Asia/Karachi': 'Карачи', + 'Indian/Kerguelen': 'Кергелен', + 'Indian/Maldives': 'Мальдивы', + 'Asia/Kolkata': 'Калькутта', + 'Asia/Colombo': 'Коломбо', + 'Asia/Kathmandu': 'Катманду', + 'Antarctica/Vostok': 'Восток', + 'Asia/Almaty': 'Алматы', + 'Asia/Urumqi': 'Урумчи', + 'Asia/Thimphu': 'Тхимпху', + 'Asia/Omsk': 'Омск', + 'Asia/Dhaka': 'Дакка', + 'Indian/Chagos': 'Чагос', + 'Asia/Bishkek': 'Бишкек', + 'Asia/Rangoon': 'Рангун', + 'Indian/Cocos': 'кокосы', + 'Asia/Bangkok': 'Бангкок', + 'Asia/Hovd': 'Ховд', + 'Asia/Novokuznetsk': 'Новокузнецк', + 'Asia/Vientiane': 'Вьентьян', + 'Asia/Krasnoyarsk': 'Красноярск', + 'Antarctica/Davis': 'Дэвис', + 'Asia/Novosibirsk': 'Новосибирск', + 'Asia/Phnom_Penh': 'Пномпень', + 'Asia/Pontianak': 'Понтианак', + 'Asia/Jakarta': 'Джакарта', + 'Asia/Ho_Chi_Minh': 'Хо Ши Мин', + 'Indian/Christmas': 'Рождество', + 'Asia/Manila': 'Манила', + 'Asia/Makassar': 'Макассар', + 'Asia/Macau': 'Макао', + 'Asia/Kuala_Lumpur': 'Куала-Лумпур', + 'Asia/Singapore': 'Сингапур', + 'Asia/Shanghai': 'Шанхай', + 'Asia/Irkutsk': 'Иркутск', + 'Asia/Kuching': 'Кучинг', + 'Asia/Hong_Kong': 'Гонконг', + 'Australia/Perth': 'Перт', + 'Asia/Taipei': 'Тайбэй', + 'Asia/Brunei': 'Бруней', + 'Asia/Choibalsan': 'Чойбалсан', + 'Asia/Ulaanbaatar': 'Улан-Батор', + 'Australia/Eucla': 'Евкла', + 'Asia/Yakutsk': 'Якутск', + 'Asia/Dili': 'Дили', + 'Pacific/Palau': 'Палау', + 'Asia/Jayapura': 'Джаяпура', + 'Asia/Seoul': 'Сеул', + 'Asia/Pyongyang': 'Пхеньян', + 'Asia/Khandyga': 'Хандыга', + 'Asia/Chita': 'Чита', + 'Asia/Tokyo': 'Токио', + 'Australia/Darwin': 'Дарвин', + 'Pacific/Saipan': 'Сайпан', + 'Australia/Brisbane': 'Брисбен', + 'Pacific/Port_Moresby': 'Порт-Морсби', + 'Pacific/Chuuk': 'Чуук', + 'Antarctica/DumontDUrville': "Дюмон-д'Юрвиль", + 'Pacific/Guam': 'Гуам', + 'Australia/Lindeman': 'Линдеман', + 'Asia/Ust-Nera': 'Усть-Нера', + 'Asia/Vladivostok': 'Владивосток', + 'Australia/Broken_Hill': 'Брокен-Хилл', + 'Australia/Adelaide': 'Аделаида', + 'Asia/Sakhalin': 'Сахалин', + 'Pacific/Guadalcanal': 'Гуадалканал', + 'Pacific/Efate': 'Эфате', + 'Antarctica/Casey': 'Кейси', + 'Antarctica/Macquarie': 'Маккуори', + 'Pacific/Kosrae': 'Косрае', + 'Australia/Sydney': 'Сидней', + 'Pacific/Noumea': 'Нумеа', + 'Australia/Melbourne': 'Мельбурн', + 'Australia/Lord_Howe': 'Остров Лорд-Хау', + 'Australia/Hobart': 'Хобарт', + 'Pacific/Pohnpei': 'Понпеи', + 'Australia/Currie': 'Карри', + 'Asia/Srednekolymsk': 'Среднеколымск', + 'Asia/Magadan': 'Магадан', + 'Pacific/Kwajalein': 'Кваджалейн', + 'Pacific/Majuro': 'Маджуро', + 'Pacific/Funafuti': 'Фунафути', + 'Asia/Anadyr': 'Анадырь', + 'Pacific/Nauru': 'Науру', + 'Asia/Kamchatka': 'Камчатка', + 'Pacific/Fiji': 'Фиджи', + 'Pacific/Norfolk': 'Норфолк', + 'Pacific/Tarawa': 'Тарава', + 'Pacific/Wallis': 'Уоллис', + 'Pacific/Wake': 'Будить', + 'Pacific/Tongatapu': 'Тонгатапу', + 'Antarctica/McMurdo': 'МакМердо', + 'Pacific/Enderbury': 'Эндербери', + 'Pacific/Fakaofo': 'Факаофо', + 'Pacific/Auckland': 'Окленд', + 'Pacific/Chatham': 'Чатем', + 'Pacific/Kiritimati': 'Киритимати', + 'Pacific/Apia': 'Апиа', }; diff --git a/lib/ui/pages/server_details/time_zone/time_zone.dart b/lib/ui/pages/server_details/time_zone/time_zone.dart index 04f192c0..33799c35 100644 --- a/lib/ui/pages/server_details/time_zone/time_zone.dart +++ b/lib/ui/pages/server_details/time_zone/time_zone.dart @@ -1,14 +1,16 @@ -part of '../server_details.dart'; +part of '../server_details_screen.dart'; final List locations = timeZoneDatabase.locations.values.toList() - ..sort((l1, l2) => - l1.currentTimeZone.offset.compareTo(l2.currentTimeZone.offset)); + ..sort( + (final l1, final l2) => + l1.currentTimeZone.offset.compareTo(l2.currentTimeZone.offset), + ); class SelectTimezone extends StatefulWidget { - SelectTimezone({Key? key}) : super(key: key); + const SelectTimezone({final super.key}); @override - _SelectTimezoneState createState() => _SelectTimezoneState(); + State createState() => _SelectTimezoneState(); } class _SelectTimezoneState extends State { @@ -16,19 +18,24 @@ class _SelectTimezoneState extends State { @override void initState() { - WidgetsBinding.instance!.addPostFrameCallback(_afterLayout); + WidgetsBinding.instance.addPostFrameCallback(_afterLayout); super.initState(); } - void _afterLayout(_) { - var t = DateTime.now().timeZoneOffset; - var index = locations.indexWhere((element) => - Duration(milliseconds: element.currentTimeZone.offset) == t); + void _afterLayout(final _) { + final t = DateTime.now().timeZoneOffset; + final index = locations.indexWhere( + (final element) => + Duration(milliseconds: element.currentTimeZone.offset) == t, + ); print(t); if (index >= 0) { - controller.animateTo(60.0 * index, - duration: Duration(milliseconds: 300), curve: Curves.easeIn); + controller.animateTo( + 60.0 * index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); } } @@ -39,69 +46,69 @@ class _SelectTimezoneState extends State { } @override - Widget build(BuildContext context) { - return Scaffold( - appBar: PreferredSize( - child: BrandHeader( - title: 'select timezone', - hasBackButton: true, + Widget build(final BuildContext context) => Scaffold( + appBar: const PreferredSize( + preferredSize: Size.fromHeight(52), + child: BrandHeader( + title: 'select timezone', + hasBackButton: true, + ), ), - preferredSize: Size.fromHeight(52), - ), - body: ListView( - controller: controller, - children: locations - .asMap() - .map((key, value) { - var duration = - Duration(milliseconds: value.currentTimeZone.offset); - var area = value.currentTimeZone.abbreviation - .replaceAll(RegExp(r'[\d+()-]'), ''); + body: ListView( + controller: controller, + children: locations + .asMap() + .map((final key, final value) { + final duration = + Duration(milliseconds: value.currentTimeZone.offset); + final area = value.currentTimeZone.abbreviation + .replaceAll(RegExp(r'[\d+()-]'), ''); - String timezoneName = value.name; - if (context.locale.toString() == 'ru') { - timezoneName = russian[value.name] ?? - () { - var arr = value.name.split('/')..removeAt(0); - return arr.join('/'); - }(); - } + String timezoneName = value.name; + if (context.locale.toString() == 'ru') { + timezoneName = russian[value.name] ?? + () { + final arr = value.name.split('/')..removeAt(0); + return arr.join('/'); + }(); + } - return MapEntry( - key, - Container( - height: 60, - padding: EdgeInsets.symmetric(horizontal: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BrandText.body1( - timezoneName, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + return MapEntry( + key, + Container( + height: 60, + padding: const EdgeInsets.symmetric(horizontal: 20), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: BrandColors.dividerColor, ), ), - BrandText.small( + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BrandText.body1( + timezoneName, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + BrandText.small( 'GMT ${duration.toDayHourMinuteFormat()} ${area.isNotEmpty ? '($area)' : ''}', - style: TextStyle( + style: const TextStyle( fontSize: 13, - )), - ], + ), + ), + ], + ), ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: BrandColors.dividerColor, - )), - ), - ), - ); - }) - .values - .toList(), - ), - ); - } + ); + }) + .values + .toList(), + ), + ); } diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index 5070bd81..0b8ea12d 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -5,7 +5,7 @@ import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/text_themes.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; @@ -19,9 +19,9 @@ import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; -import '../rootRoute.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; const switchableServices = [ ServiceTypes.passwordManager, @@ -32,20 +32,19 @@ const switchableServices = [ ]; class ServicesPage extends StatefulWidget { - ServicesPage({Key? key}) : super(key: key); + const ServicesPage({final super.key}); @override - _ServicesPageState createState() => _ServicesPageState(); + State createState() => _ServicesPageState(); } -void _launchURL(url) async { - var _possible = await canLaunch(url); +void _launchURL(final url) async { + final canLaunch = await canLaunchUrlString(url); - if (_possible) { + if (canLaunch) { try { - await launch( + await launchUrlString( url, - enableJavaScript: true, ); } catch (e) { print(e); @@ -57,30 +56,32 @@ void _launchURL(url) async { class _ServicesPageState extends State { @override - Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; + Widget build(final BuildContext context) { + final isReady = context.watch().state + is ServerInstallationFinished; return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.services'.tr(), - hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), body: ListView( padding: paddingH15V0, children: [ BrandText.body1('services.title'.tr()), - SizedBox(height: 24), - if (!isReady) ...[NotReadyCard(), SizedBox(height: 24)], + const SizedBox(height: 24), + if (!isReady) ...[const NotReadyCard(), const SizedBox(height: 24)], ...ServiceTypes.values - .map((t) => Padding( - padding: EdgeInsets.only( - bottom: 30, - ), - child: _Card(serviceType: t), - )) + .map( + (final t) => Padding( + padding: const EdgeInsets.only( + bottom: 30, + ), + child: _Card(serviceType: t), + ), + ) .toList() ], ), @@ -89,30 +90,32 @@ class _ServicesPageState extends State { } class _Card extends StatelessWidget { - const _Card({Key? key, required this.serviceType}) : super(key: key); + const _Card({required this.serviceType}); final ServiceTypes serviceType; @override - Widget build(BuildContext context) { - var isReady = context.watch().state is AppConfigFinished; - var changeTab = context.read().onPress; + Widget build(final BuildContext context) { + final isReady = context.watch().state + is ServerInstallationFinished; + final changeTab = context.read().onPress; - var serviceState = context.watch().state; - var jobsCubit = context.watch(); - var jobState = jobsCubit.state; + final serviceState = context.watch().state; + final jobsCubit = context.watch(); + final jobState = jobsCubit.state; - var switchableService = switchableServices.contains(serviceType); - var hasSwitchJob = switchableService && + final switchableService = switchableServices.contains(serviceType); + final hasSwitchJob = switchableService && jobState is JobsStateWithJobs && - jobState.jobList - .any((el) => el is ServiceToggleJob && el.type == serviceType); + jobState.jobList.any( + (final el) => el is ServiceToggleJob && el.type == serviceType, + ); - var isSwitchOn = isReady && + final isSwitchOn = isReady && (!switchableServices.contains(serviceType) || serviceState.isEnableByType(serviceType)); - var config = context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final config = context.watch().state; + final domainName = UiHelpers.getDomainName(config); return GestureDetector( onTap: isSwitchOn @@ -120,16 +123,14 @@ class _Card extends StatelessWidget { context: context, // isScrollControlled: true, // backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return _ServiceDetails( - serviceType: serviceType, - status: - isSwitchOn ? StateType.stable : StateType.uninitialized, - title: serviceType.title, - icon: serviceType.icon, - changeTab: changeTab, - ); - }, + builder: (final BuildContext context) => _ServiceDetails( + serviceType: serviceType, + status: + isSwitchOn ? StateType.stable : StateType.uninitialized, + title: serviceType.title, + icon: serviceType.icon, + changeTab: changeTab, + ), ) : null, child: BrandCards.big( @@ -144,16 +145,15 @@ class _Card extends StatelessWidget { child: Icon(serviceType.icon, size: 30, color: Colors.white), ), if (isReady && switchableService) ...[ - Spacer(), + const Spacer(), Builder( - builder: (context) { + builder: (final context) { late bool isActive; if (hasSwitchJob) { - isActive = ((jobState as JobsStateWithJobs) - .jobList - .firstWhere((el) => - el is ServiceToggleJob && - el.type == serviceType) as ServiceToggleJob) + isActive = (jobState.jobList.firstWhere( + (final el) => + el is ServiceToggleJob && el.type == serviceType, + ) as ServiceToggleJob) .needToTurnOn; } else { isActive = serviceState.isEnableByType(serviceType); @@ -161,7 +161,7 @@ class _Card extends StatelessWidget { return BrandSwitch( value: isActive, - onChanged: (value) => + onChanged: (final value) => jobsCubit.createOrRemoveServiceToggleJob( ServiceToggleJob( type: serviceType, @@ -180,35 +180,45 @@ class _Card extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2(serviceType.title), - SizedBox(height: 10), + const SizedBox(height: 10), if (serviceType.subdomain != '') Column( children: [ GestureDetector( onTap: () => _launchURL( - 'https://${serviceType.subdomain}.$domainName'), + 'https://${serviceType.subdomain}.$domainName', + ), child: Text( '${serviceType.subdomain}.$domainName', - style: linkStyle, + style: TextStyle( + color: + Theme.of(context).colorScheme.secondary, + decoration: TextDecoration.underline, + ), ), ), - SizedBox(height: 10), + const SizedBox(height: 10), ], ), if (serviceType == ServiceTypes.mail) - Column(children: [ - Text( - domainName, - style: linkStyle, - ), - SizedBox(height: 10), - ]), + Column( + children: [ + Text( + domainName, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + ), + const SizedBox(height: 10), + ], + ), BrandText.body2(serviceType.loginInfo), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.body2(serviceType.subtitle), - SizedBox(height: 10), + const SizedBox(height: 10), ], ), if (hasSwitchJob) @@ -239,13 +249,12 @@ class _Card extends StatelessWidget { class _ServiceDetails extends StatelessWidget { const _ServiceDetails({ - Key? key, required this.serviceType, required this.icon, required this.status, required this.title, required this.changeTab, - }) : super(key: key); + }); final ServiceTypes serviceType; final IconData icon; @@ -254,13 +263,13 @@ class _ServiceDetails extends StatelessWidget { final ValueChanged changeTab; @override - Widget build(BuildContext context) { + Widget build(final BuildContext context) { late Widget child; - var config = context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final config = context.watch().state; + final domainName = UiHelpers.getDomainName(config); - var linksStyle = body1Style.copyWith( + final linksStyle = body1Style.copyWith( fontSize: 15, color: Theme.of(context).brightness == Brightness.dark ? Colors.white @@ -269,7 +278,7 @@ class _ServiceDetails extends StatelessWidget { decoration: TextDecoration.underline, ); - var textStyle = body1Style.copyWith( + final textStyle = body1Style.copyWith( color: Theme.of(context).brightness == Brightness.dark ? Colors.white : BrandColors.black, @@ -277,163 +286,171 @@ class _ServiceDetails extends StatelessWidget { switch (serviceType) { case ServiceTypes.mail: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - child: Text( - 'services.mail.bottom_sheet.2'.tr(), - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + child: Text( + 'services.mail.bottom_sheet.2'.tr(), + style: linksStyle, + ), + onTap: () { + Navigator.of(context).pop(); + changeTab(2); + }, ), - onTap: () { - Navigator.of(context).pop(); - changeTab(2); - }, ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.messenger: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.messenger.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ) - ], - )); + text: TextSpan( + children: [ + TextSpan( + text: + 'services.messenger.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ) + ], + ), + ); break; case ServiceTypes.passwordManager: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.password_manager.bottom_sheet.1' - .tr(args: [domainName]), - style: textStyle, - ), - WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://password.$domainName'), - child: Text( - 'password.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.password_manager.bottom_sheet.1' + .tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://password.$domainName'), + child: Text( + 'password.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.video: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.video.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://meet.$domainName'), - child: Text( - 'meet.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.video.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://meet.$domainName'), + child: Text( + 'meet.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.cloud: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://cloud.$domainName'), - child: Text( - 'cloud.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.cloud.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://cloud.$domainName'), + child: Text( + 'cloud.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.socialNetwork: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.social_network.bottom_sheet.1' - .tr(args: [domainName]), - style: textStyle, - ), - WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://social.$domainName'), - child: Text( - 'social.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.social_network.bottom_sheet.1' + .tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://social.$domainName'), + child: Text( + 'social.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.git: child = RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'services.git.bottom_sheet.1'.tr(args: [domainName]), - style: textStyle, - ), - WidgetSpan(child: SizedBox(width: 5)), - WidgetSpan( - child: Padding( - padding: EdgeInsets.only(bottom: 0.8), - child: GestureDetector( - onTap: () => _launchURL('https://git.$domainName'), - child: Text( - 'git.$domainName', - style: linksStyle, + text: TextSpan( + children: [ + TextSpan( + text: 'services.git.bottom_sheet.1'.tr(args: [domainName]), + style: textStyle, + ), + const WidgetSpan(child: SizedBox(width: 5)), + WidgetSpan( + child: Padding( + padding: const EdgeInsets.only(bottom: 0.8), + child: GestureDetector( + onTap: () => _launchURL('https://git.$domainName'), + child: Text( + 'git.$domainName', + style: linksStyle, + ), ), ), ), - ), - ], - )); + ], + ), + ); break; case ServiceTypes.vpn: child = Text( @@ -446,7 +463,7 @@ class _ServiceDetails extends StatelessWidget { borderRadius: BorderRadius.circular(20), ), child: SingleChildScrollView( - child: Container( + child: SizedBox( width: 350, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -460,11 +477,11 @@ class _ServiceDetails extends StatelessWidget { status: status, child: Icon(icon, size: 40, color: Colors.white), ), - SizedBox(height: 10), + const SizedBox(height: 10), BrandText.h2(title), - SizedBox(height: 10), + const SizedBox(height: 10), child, - SizedBox(height: 40), + const SizedBox(height: 40), Center( child: Container( child: BrandButton.rised( diff --git a/lib/ui/pages/setup/initializing.dart b/lib/ui/pages/setup/initializing.dart new file mode 100644 index 00000000..9c92f161 --- /dev/null +++ b/lib/ui/pages/setup/initializing.dart @@ -0,0 +1,580 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/domain_cloudflare.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/root_user_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:selfprivacy/ui/components/brand_timer/brand_timer.dart'; +import 'package:selfprivacy/ui/components/progress_bar/progress_bar.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_routing.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class InitializingPage extends StatelessWidget { + const InitializingPage({final super.key}); + + @override + Widget build(final BuildContext context) { + final cubit = context.watch(); + + if (cubit.state is ServerInstallationRecovery) { + return const RecoveryRouting(); + } else { + final actualInitializingPage = [ + () => _stepHetzner(cubit), + () => _stepCloudflare(cubit), + () => _stepBackblaze(cubit), + () => _stepDomain(cubit), + () => _stepUser(cubit), + () => _stepServer(cubit), + () => _stepCheck(cubit), + () => _stepCheck(cubit), + () => _stepCheck(cubit), + () => Center(child: Text('initializing.finish'.tr())) + ][cubit.state.progress.index](); + + return BlocListener( + listener: (final context, final state) { + if (cubit.state is ServerInstallationFinished) { + Navigator.of(context) + .pushReplacement(materialRoute(const RootPage())); + } + }, + child: SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: paddingH15V0.copyWith(top: 10, bottom: 10), + child: cubit.state.isFullyInitilized + ? const SizedBox( + height: 80, + ) + : ProgressBar( + steps: const [ + 'Hetzner', + 'CloudFlare', + 'Backblaze', + 'Domain', + 'User', + 'Server', + '✅ Check', + ], + activeIndex: cubit.state.porgressBar, + ), + ), + _addCard( + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: actualInitializingPage, + ), + ), + ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height - + MediaQuery.of(context).padding.top - + MediaQuery.of(context).padding.bottom - + 566, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + alignment: Alignment.center, + child: BrandButton.text( + title: cubit.state is ServerInstallationFinished + ? 'basis.close'.tr() + : 'basis.later'.tr(), + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(const RootPage()), + (final predicate) => false, + ); + }, + ), + ), + if (cubit.state is ServerInstallationFinished) + Container() + else + Container( + alignment: Alignment.center, + child: BrandButton.text( + title: 'basis.connect_to_existing'.tr(), + onPressed: () { + Navigator.of(context).push( + materialRoute( + const RecoveryRouting(), + ), + ); + }, + ), + ) + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + } + + Widget _stepHetzner(final ServerInstallationCubit serverInstallationCubit) => + BlocProvider( + create: (final context) => HetznerFormCubit(serverInstallationCubit), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/hetzner.png', + width: 150, + ), + const SizedBox(height: 10), + BrandText.h2('initializing.1'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.2'.tr()), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().apiKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Hetzner API Token', + ), + ), + const Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () => _showModal( + context, + const _HowTo(fileName: 'how_hetzner'), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); + + void _showModal(final BuildContext context, final Widget widget) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => widget, + ); + } + + Widget _stepCloudflare(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => CloudFlareFormCubit(initializingCubit), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/cloudflare.png', + width: 150, + ), + const SizedBox(height: 10), + BrandText.h2('initializing.3'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.4'.tr()), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().apiKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'initializing.5'.tr(), + ), + ), + const Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () => _showModal( + context, + const _HowTo( + fileName: 'how_cloudflare', + ), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); + + Widget _stepBackblaze(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => BackblazeFormCubit(initializingCubit), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/backblaze.png', + height: 50, + ), + const SizedBox(height: 10), + BrandText.h2('initializing.6'.tr()), + const SizedBox(height: 10), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().keyId, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'KeyID', + ), + ), + const Spacer(), + CubitFormTextField( + formFieldCubit: + context.read().applicationKey, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: const InputDecoration( + hintText: 'Master Application Key', + ), + ), + const Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 10), + BrandButton.text( + onPressed: () => _showModal( + context, + const _HowTo( + fileName: 'how_backblaze', + ), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); + + Widget _stepDomain(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => DomainSetupCubit(initializingCubit)..load(), + child: Builder( + builder: (final context) { + final DomainSetupState state = + context.watch().state; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset( + 'assets/images/logos/cloudflare.png', + width: 150, + ), + const SizedBox(height: 30), + BrandText.h2('basis.domain'.tr()), + const SizedBox(height: 10), + if (state is Empty) BrandText.body2('initializing.7'.tr()), + if (state is Loading) + BrandText.body2( + state.type == LoadingTypes.loadingDomain + ? 'initializing.8'.tr() + : 'basis.saving'.tr(), + ), + if (state is MoreThenOne) + BrandText.body2( + 'initializing.9'.tr(), + ), + if (state is Loaded) ...[ + const SizedBox(height: 10), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: BrandText.h3( + state.domain, + textAlign: TextAlign.center, + ), + ), + SizedBox( + width: 56, + child: BrandButton.rised( + onPressed: () => + context.read().load(), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: const [ + Icon( + Icons.refresh, + color: Colors.white, + ), + ], + ), + ), + ), + ], + ) + ], + if (state is Empty) ...[ + const SizedBox(height: 30), + BrandButton.rised( + onPressed: () => context.read().load(), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.refresh, + color: Colors.white, + ), + const SizedBox(width: 10), + BrandText.buttonTitleText('Обновить cписок'), + ], + ), + ), + ], + if (state is Loaded) ...[ + const SizedBox(height: 30), + BrandButton.rised( + onPressed: () => + context.read().saveDomain(), + text: 'initializing.10'.tr(), + ), + ], + const SizedBox( + height: 10, + width: double.infinity, + ), + ], + ); + }, + ), + ); + + Widget _stepUser(final ServerInstallationCubit initializingCubit) => + BlocProvider( + create: (final context) => + RootUserFormCubit(initializingCubit, FieldCubitFactory(context)), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + BrandText.h2('initializing.22'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.23'.tr()), + const Spacer(), + CubitFormTextField( + formFieldCubit: context.read().userName, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'basis.nickname'.tr(), + ), + ), + const SizedBox(height: 10), + BlocBuilder, FieldCubitState>( + bloc: context.read().isVisible, + builder: (final context, final state) { + final bool isVisible = state.value; + return CubitFormTextField( + obscureText: !isVisible, + formFieldCubit: + context.read().password, + textAlign: TextAlign.center, + scrollPadding: const EdgeInsets.only(bottom: 70), + decoration: InputDecoration( + hintText: 'basis.password'.tr(), + suffixIcon: IconButton( + icon: Icon( + isVisible ? Icons.visibility : Icons.visibility_off, + ), + onPressed: () => context + .read() + .isVisible + .setValue(!isVisible), + ), + suffixIconConstraints: + const BoxConstraints(minWidth: 60), + prefixIconConstraints: + const BoxConstraints(maxWidth: 60), + prefixIcon: Container(), + ), + ); + }, + ), + const Spacer(), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + ], + ); + }, + ), + ); + + Widget _stepServer(final ServerInstallationCubit appConfigCubit) { + final bool isLoading = + (appConfigCubit.state as ServerInstallationNotFinished).isLoading; + return Builder( + builder: (final context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(flex: 2), + BrandText.h2('initializing.final'.tr()), + const SizedBox(height: 10), + BrandText.body2('initializing.11'.tr()), + const Spacer(), + BrandButton.rised( + onPressed: + isLoading ? null : appConfigCubit.createServerAndSetDnsRecords, + text: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(), + ), + ], + ), + ); + } + + Widget _stepCheck(final ServerInstallationCubit appConfigCubit) { + assert( + appConfigCubit.state is ServerInstallationNotFinished, + 'wrong state', + ); + final state = appConfigCubit.state as TimerState; + late int doneCount; + late String? text; + if (state.isServerResetedSecondTime) { + text = 'initializing.13'.tr(); + doneCount = 3; + } else if (state.isServerResetedFirstTime) { + text = 'initializing.21'.tr(); + doneCount = 2; + } else if (state.isServerStarted) { + text = 'initializing.14'.tr(); + doneCount = 1; + } else if (state.isServerCreated) { + text = 'initializing.15'.tr(); + doneCount = 0; + } + return Builder( + builder: (final context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 15), + BrandText.h4( + 'initializing.checks'.tr(args: [doneCount.toString(), '4']), + ), + const Spacer(flex: 2), + const SizedBox(height: 10), + BrandText.body2(text), + const SizedBox(height: 10), + if (doneCount == 0 && state.dnsMatches != null) + Column( + children: state.dnsMatches!.entries.map((final entry) { + final String domain = entry.key; + final bool isCorrect = entry.value; + return Row( + children: [ + if (isCorrect) const Icon(Icons.check, color: Colors.green), + if (!isCorrect) + const Icon(Icons.schedule, color: Colors.amber), + const SizedBox(width: 10), + Text(domain), + ], + ); + }).toList(), + ), + const SizedBox(height: 10), + if (!state.isLoading) + Row( + children: [ + BrandText.body2('initializing.16'.tr()), + BrandTimer( + startDateTime: state.timerStart!, + duration: state.duration!, + ) + ], + ), + if (state.isLoading) BrandText.body2('initializing.17'.tr()), + ], + ), + ); + } + + Widget _addCard(final Widget child) => Container( + height: 450, + padding: paddingH15V0, + child: BrandCards.big(child: child), + ); +} + +class _HowTo extends StatelessWidget { + const _HowTo({ + required this.fileName, + }); + + final String fileName; + + @override + Widget build(final BuildContext context) => BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: [ + BrandMarkdown( + fileName: fileName, + ), + ], + ), + ), + ); +} diff --git a/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart new file mode 100644 index 00000000..a9f37b19 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recover_by_new_device_key.dart @@ -0,0 +1,89 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; + +class RecoverByNewDeviceKeyInstruction extends StatelessWidget { + const RecoverByNewDeviceKeyInstruction({final super.key}); + + @override + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: + context.read().revertRecoveryStep, + children: [ + FilledButton( + title: 'recovering.method_device_button'.tr(), + onPressed: () => Navigator.of(context) + .push(materialRoute(const RecoverByNewDeviceKeyInput())), + ) + ], + ); +} + +class RecoverByNewDeviceKeyInput extends StatelessWidget { + const RecoverByNewDeviceKeyInput({final super.key}); + + @override + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); + + return BlocProvider( + create: (final BuildContext context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.newDeviceKey, + ), + child: BlocListener( + listener: + (final BuildContext context, final ServerInstallationState state) { + if (state is ServerInstallationRecovery && + state.currentStep != RecoveryStep.newDeviceKey) { + Navigator.of(context).pop(); + } + }, + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; + + return BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_input_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: + 'recovering.method_device_input_placeholder'.tr(), + ), + ), + const SizedBox(height: 16), + FilledButton( + title: 'more.continue'.tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => + context.read().trySubmit(), + ) + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recover_by_old_token.dart b/lib/ui/pages/setup/recovering/recover_by_old_token.dart new file mode 100644 index 00000000..e3507a0e --- /dev/null +++ b/lib/ui/pages/setup/recovering/recover_by_old_token.dart @@ -0,0 +1,97 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; + +class RecoverByOldTokenInstruction extends StatelessWidget { + @override + const RecoverByOldTokenInstruction({ + required this.instructionFilename, + final super.key, + }); + + @override + Widget build(final BuildContext context) => + BlocListener( + listener: (final context, final state) { + if (state is ServerInstallationRecovery && + state.currentStep != RecoveryStep.selecting) { + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: + context.read().revertRecoveryStep, + children: [ + BrandMarkdown( + fileName: instructionFilename, + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovering.method_device_button'.tr(), + onPressed: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.oldToken), + ) + ], + ), + ); + + final String instructionFilename; +} + +class RecoverByOldToken extends StatelessWidget { + const RecoverByOldToken({final super.key}); + + @override + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); + + return BlocProvider( + create: (final context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.oldToken, + ), + child: Builder( + builder: (final context) { + final FormCubitState formCubitState = + context.watch().state; + + return BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_device_input_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'recovering.method_device_input_placeholder'.tr(), + ), + ), + const SizedBox(height: 16), + FilledButton( + title: 'more.continue'.tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart new file mode 100644 index 00000000..f729524e --- /dev/null +++ b/lib/ui/pages/setup/recovering/recover_by_recovery_key.dart @@ -0,0 +1,58 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_device_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class RecoverByRecoveryKey extends StatelessWidget { + const RecoverByRecoveryKey({final super.key}); + + @override + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); + + return BlocProvider( + create: (final context) => RecoveryDeviceFormCubit( + appConfig, + FieldCubitFactory(context), + ServerRecoveryMethods.recoveryKey, + ), + child: Builder( + builder: (final context) { + final FormCubitState formCubitState = + context.watch().state; + + return BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_recovery_input_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: + context.read().revertRecoveryStep, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().tokenField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'recovering.method_device_input_placeholder'.tr(), + ), + ), + const SizedBox(height: 16), + FilledButton( + title: 'more.continue'.tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart new file mode 100644 index 00000000..2b558727 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_backblaze.dart @@ -0,0 +1,85 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/backblaze_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; + +class RecoveryConfirmBackblaze extends StatelessWidget { + const RecoveryConfirmBackblaze({final super.key}); + + @override + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); + + return BlocProvider( + create: (final BuildContext context) => BackblazeFormCubit(appConfig), + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; + + return BrandHeroScreen( + heroTitle: 'recovering.confirm_backblaze'.tr(), + heroSubtitle: 'recovering.confirm_backblaze_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().keyId, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'KeyID', + ), + ), + const SizedBox(height: 16), + CubitFormTextField( + formFieldCubit: + context.read().applicationKey, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Master Application Key', + ), + ), + const SizedBox(height: 16), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 16), + BrandButton.text( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: const [ + BrandMarkdown( + fileName: 'how_backblaze', + ), + ], + ), + ), + ), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart new file mode 100644 index 00000000..8cbdbe6c --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart @@ -0,0 +1,78 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/cloudflare_form_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; + +class RecoveryConfirmCloudflare extends StatelessWidget { + const RecoveryConfirmCloudflare({final super.key}); + + @override + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); + + return BlocProvider( + create: (final BuildContext context) => CloudFlareFormCubit(appConfig), + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; + + return BrandHeroScreen( + heroTitle: 'recovering.confirm_cloudflare'.tr(), + heroSubtitle: 'recovering.confirm_cloudflare_description'.tr( + args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], + ), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'initializing.5'.tr(), + ), + ), + const SizedBox(height: 16), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.connect'.tr(), + ), + const SizedBox(height: 16), + BrandButton.text( + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: const [ + BrandMarkdown( + fileName: 'how_cloudflare', + ), + ], + ), + ), + ), + ), + title: 'initializing.how'.tr(), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_confirm_server.dart b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart new file mode 100644 index 00000000..110425ef --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_confirm_server.dart @@ -0,0 +1,298 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/server_basic_info.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; + +class RecoveryConfirmServer extends StatefulWidget { + const RecoveryConfirmServer({final super.key}); + + @override + State createState() => _RecoveryConfirmServerState(); +} + +class _RecoveryConfirmServerState extends State { + bool _isExtended = false; + + bool _isServerFound(final List servers) => + servers + .where((final server) => server.isIpValid && server.isReverseDnsValid) + .length == + 1; + + ServerBasicInfoWithValidators _firstValidServer( + final List servers, + ) => + servers + .where((final server) => server.isIpValid && server.isReverseDnsValid) + .first; + + @override + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: _isExtended + ? 'recovering.choose_server'.tr() + : 'recovering.confirm_server'.tr(), + heroSubtitle: _isExtended + ? 'recovering.choose_server_description'.tr() + : 'recovering.confirm_server_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + FutureBuilder>( + future: context + .read() + .getServersOnHetznerAccount(), + builder: (final context, final snapshot) { + if (snapshot.hasData) { + final servers = snapshot.data; + return Column( + children: [ + if (servers != null && servers.isNotEmpty) + Column( + children: [ + if (servers.length == 1 || + (!_isExtended && _isServerFound(servers))) + confirmServer( + context, + _firstValidServer(servers), + servers.length > 1, + ), + if (servers.length > 1 && + (_isExtended || !_isServerFound(servers))) + chooseServer(context, servers), + ], + ), + if (servers?.isEmpty ?? true) + Center( + child: Text( + 'recovering.no_servers'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ], + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ) + ], + ); + + Widget confirmServer( + final BuildContext context, + final ServerBasicInfoWithValidators server, + final bool showMoreServersButton, + ) => + Column( + children: [ + serverCard( + context: context, + server: server, + ), + const SizedBox(height: 16), + FilledButton( + title: 'recovering.confirm_server_accept'.tr(), + onPressed: () => _showConfirmationDialog(context, server), + ), + const SizedBox(height: 16), + if (showMoreServersButton) + BrandButton.text( + title: 'recovering.confirm_server_decline'.tr(), + onPressed: () => setState(() => _isExtended = true), + ), + ], + ); + + Widget chooseServer( + final BuildContext context, + final List servers, + ) => + Column( + children: [ + for (final server in servers) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: serverCard( + context: context, + server: server, + onTap: () => _showConfirmationDialog(context, server), + ), + ), + ], + ); + + Widget serverCard({ + required final BuildContext context, + required final ServerBasicInfoWithValidators server, + final VoidCallback? onTap, + }) => + BrandCards.filled( + child: ListTile( + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 16), + onTap: onTap, + title: Text( + server.name, + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + leading: Icon( + Icons.dns_outlined, + color: Theme.of(context).colorScheme.onSurface, + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + server.isReverseDnsValid ? Icons.check : Icons.close, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'rDNS: ${server.reverseDns}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + Row( + children: [ + Icon( + server.isIpValid ? Icons.check : Icons.close, + color: Theme.of(context).colorScheme.onSurface, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'IP: ${server.ip}', + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), + ), + ); + + Future _showConfirmationDialog( + final BuildContext context, + final ServerBasicInfoWithValidators server, + ) => + showDialog( + context: context, + builder: (final context) => AlertDialog( + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Icons.warning_amber_outlined), + const SizedBox(height: 16), + Text( + 'recovering.modal_confirmation_title'.tr(), + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'recovering.modal_confirmation_description'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 12), + Text( + server.name, + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.start, + ), + const SizedBox(height: 8), + IsValidStringDisplay( + isValid: server.isReverseDnsValid, + textIfValid: 'recovering.modal_confirmation_dns_valid'.tr(), + textIfInvalid: 'recovering.modal_confirmation_dns_invalid'.tr(), + ), + const SizedBox(height: 8), + IsValidStringDisplay( + isValid: server.isIpValid, + textIfValid: 'recovering.modal_confirmation_ip_valid'.tr(), + textIfInvalid: 'recovering.modal_confirmation_ip_invalid'.tr(), + ), + ], + ), + actions: [ + TextButton( + child: Text('modals.no'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text('modals.yes'.tr()), + onPressed: () { + context.read().setServerId(server); + Navigator.of(context).pop(); + }, + ), + ], + ), + ); +} + +class IsValidStringDisplay extends StatelessWidget { + const IsValidStringDisplay({ + required this.isValid, + required this.textIfValid, + required this.textIfInvalid, + final super.key, + }); + + final bool isValid; + final String textIfValid; + final String textIfInvalid; + + @override + Widget build(final BuildContext context) => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isValid) + Icon(Icons.check, color: Theme.of(context).colorScheme.onSurface) + else + Icon(Icons.close, color: Theme.of(context).colorScheme.error), + const SizedBox(width: 8), + Expanded( + child: isValid + ? Text( + textIfValid, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ) + : Text( + textIfInvalid, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + ], + ); +} diff --git a/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart new file mode 100644 index 00000000..e1812b32 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_hentzner_connected.dart @@ -0,0 +1,79 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/initializing/hetzner_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_md/brand_md.dart'; + +class RecoveryHetznerConnected extends StatelessWidget { + const RecoveryHetznerConnected({final super.key}); + + @override + Widget build(final BuildContext context) { + final ServerInstallationCubit appConfig = + context.watch(); + + return BlocProvider( + create: (final BuildContext context) => HetznerFormCubit(appConfig), + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; + + return BrandHeroScreen( + heroTitle: 'recovering.hetzner_connected'.tr(), + heroSubtitle: 'recovering.hetzner_connected_description'.tr( + args: [appConfig.state.serverDomain?.domainName ?? 'your domain'], + ), + hasBackButton: true, + hasFlashButton: false, + children: [ + CubitFormTextField( + formFieldCubit: context.read().apiKey, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'recovering.hetzner_connected_placeholder'.tr(), + ), + ), + const SizedBox(height: 16), + FilledButton( + title: 'more.continue'.tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + ), + const SizedBox(height: 16), + BrandButton.text( + title: 'initializing.how'.tr(), + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => BrandBottomSheet( + isExpended: true, + child: Padding( + padding: paddingH15V0, + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 16), + children: const [ + BrandMarkdown( + fileName: 'how_hetzner', + ), + ], + ), + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/setup/recovering/recovery_method_select.dart b/lib/ui/pages/setup/recovering/recovery_method_select.dart new file mode 100644 index 00000000..fe622acb --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_method_select.dart @@ -0,0 +1,135 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class RecoveryMethodSelect extends StatelessWidget { + const RecoveryMethodSelect({final super.key}); + + @override + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.method_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: + context.read().revertRecoveryStep, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.method_select_other_device'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.offline_share_outlined), + onTap: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.newDeviceKey), + ), + ), + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.method_select_recovery_key'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.password_outlined), + onTap: () => context + .read() + .selectRecoveryMethod(ServerRecoveryMethods.recoveryKey), + ), + ), + const SizedBox(height: 16), + BrandButton.text( + title: 'recovering.method_select_nothing'.tr(), + onPressed: () => Navigator.of(context) + .push(materialRoute(const RecoveryFallbackMethodSelect())), + ) + ], + ); +} + +class RecoveryFallbackMethodSelect extends StatelessWidget { + const RecoveryFallbackMethodSelect({final super.key}); + + @override + Widget build(final BuildContext context) => + BlocListener( + listener: (final context, final state) { + if (state is ServerInstallationRecovery && + state.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens && + state.currentStep != RecoveryStep.selecting) { + Navigator.of(context).pop(); + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.fallback_select_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_token_copy'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.vpn_key), + onTap: () => Navigator.of(context).push( + materialRoute( + const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_old', + ), + ), + ), + ), + ), + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_root_ssh'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + leading: const Icon(Icons.terminal), + onTap: () => Navigator.of(context).push( + materialRoute( + const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_ssh', + ), + ), + ), + ), + ), + const SizedBox(height: 16), + BrandCards.outlined( + child: ListTile( + title: Text( + 'recovering.fallback_select_provider_console'.tr(), + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + 'recovering.fallback_select_provider_console_hint'.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + leading: const Icon(Icons.web), + onTap: () => Navigator.of(context).push( + materialRoute( + const RecoverByOldTokenInstruction( + instructionFilename: 'how_fallback_terminal', + ), + ), + ), + ), + ), + ], + ), + ); +} diff --git a/lib/ui/pages/setup/recovering/recovery_routing.dart b/lib/ui/pages/setup/recovering/recovery_routing.dart new file mode 100644 index 00000000..3c375ef0 --- /dev/null +++ b/lib/ui/pages/setup/recovering/recovery_routing.dart @@ -0,0 +1,143 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; +import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; +import 'package:selfprivacy/logic/cubit/forms/setup/recovering/recovery_domain_form_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_button/filled_button.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/pages/root_route.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_old_token.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_recovery_key.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recover_by_new_device_key.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_backblaze.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_cloudflare.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_confirm_server.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_hentzner_connected.dart'; +import 'package:selfprivacy/ui/pages/setup/recovering/recovery_method_select.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; + +class RecoveryRouting extends StatelessWidget { + const RecoveryRouting({final super.key}); + + @override + Widget build(final BuildContext context) { + final serverInstallation = context.watch().state; + + Widget currentPage = const SelectDomainToRecover(); + + if (serverInstallation is ServerInstallationRecovery) { + switch (serverInstallation.currentStep) { + case RecoveryStep.selecting: + if (serverInstallation.recoveryCapabilities == + ServerRecoveryCapabilities.loginTokens) { + currentPage = const RecoveryMethodSelect(); + } + if (serverInstallation.recoveryCapabilities == + ServerRecoveryCapabilities.legacy) { + currentPage = const RecoveryFallbackMethodSelect(); + } + break; + case RecoveryStep.recoveryKey: + currentPage = const RecoverByRecoveryKey(); + break; + case RecoveryStep.newDeviceKey: + currentPage = const RecoverByNewDeviceKeyInstruction(); + break; + case RecoveryStep.oldToken: + currentPage = const RecoverByOldToken(); + break; + case RecoveryStep.hetznerToken: + currentPage = const RecoveryHetznerConnected(); + break; + case RecoveryStep.serverSelection: + currentPage = const RecoveryConfirmServer(); + break; + case RecoveryStep.cloudflareToken: + currentPage = const RecoveryConfirmCloudflare(); + break; + case RecoveryStep.backblazeToken: + currentPage = const RecoveryConfirmBackblaze(); + break; + } + } + + return BlocListener( + listener: (final context, final state) { + if (state is ServerInstallationFinished) { + Navigator.of(context).popUntil((final route) => route.isFirst); + } + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: currentPage, + ), + ); + } +} + +class SelectDomainToRecover extends StatelessWidget { + const SelectDomainToRecover({final super.key}); + + @override + Widget build(final BuildContext context) { + final serverInstallation = context.watch(); + + return BlocProvider( + create: (final context) => RecoveryDomainFormCubit( + serverInstallation, + FieldCubitFactory(context), + ), + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; + + return BlocListener( + listener: (final context, final state) { + if (state is ServerInstallationRecovery) { + if (state.currentStep == RecoveryStep.selecting) { + if (state.recoveryCapabilities == + ServerRecoveryCapabilities.none) { + context + .read() + .setCustomError('recovering.domain_recover_error'.tr()); + } + } + } + }, + child: BrandHeroScreen( + heroTitle: 'recovering.recovery_main_header'.tr(), + heroSubtitle: 'recovering.domain_recovery_description'.tr(), + hasBackButton: true, + hasFlashButton: false, + onBackButtonPressed: () { + Navigator.of(context).pushAndRemoveUntil( + materialRoute(const RootPage()), + (final predicate) => false, + ); + }, + children: [ + CubitFormTextField( + formFieldCubit: + context.read().serverDomainField, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: 'recovering.domain_recover_placeholder'.tr(), + ), + ), + const SizedBox(height: 16), + FilledButton( + title: 'more.continue'.tr(), + onPressed: formCubitState.isSubmitting + ? null + : () => + context.read().trySubmit(), + ) + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/ui/pages/ssh_keys/new_ssh_key.dart b/lib/ui/pages/ssh_keys/new_ssh_key.dart index 4d7a3625..247590b7 100644 --- a/lib/ui/pages/ssh_keys/new_ssh_key.dart +++ b/lib/ui/pages/ssh_keys/new_ssh_key.dart @@ -1,76 +1,76 @@ part of 'ssh_keys.dart'; class _NewSshKey extends StatelessWidget { + const _NewSshKey(this.user); final User user; - _NewSshKey(this.user); - @override - Widget build(BuildContext context) { - return BrandBottomSheet( - child: BlocProvider( - create: (context) { - var jobCubit = context.read(); - var jobState = jobCubit.state; - if (jobState is JobsStateWithJobs) { - var jobs = jobState.jobList; - jobs.forEach((job) { - if (job is CreateSSHKeyJob && job.user.login == user.login) { - user.sshKeys.add(job.publicKey); + Widget build(final BuildContext context) => BrandBottomSheet( + child: BlocProvider( + create: (final context) { + final jobCubit = context.read(); + final jobState = jobCubit.state; + if (jobState is JobsStateWithJobs) { + final jobs = jobState.jobList; + for (final job in jobs) { + if (job is CreateSSHKeyJob && job.user.login == user.login) { + user.sshKeys.add(job.publicKey); + } } - }); - } - return SshFormCubit( - jobsCubit: jobCubit, - user: user, - ); - }, - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + } + return SshFormCubit( + jobsCubit: jobCubit, + user: user, + ); + }, + child: Builder( + builder: (final context) { + final formCubitState = context.watch().state; - return BlocListener( - listener: (context, state) { - if (state.isSubmitted) { - Navigator.pop(context); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BrandHeader( - title: user.login, - ), - SizedBox(width: 14), - Padding( - padding: paddingH15V0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: CubitFormTextField( - formFieldCubit: context.read().key, - decoration: InputDecoration( - labelText: 'ssh.input_label'.tr(), + return BlocListener( + listener: (final context, final state) { + if (state.isSubmitted) { + Navigator.pop(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BrandHeader( + title: user.login, + ), + const SizedBox(width: 14), + Padding( + padding: paddingH15V0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().key, + decoration: InputDecoration( + labelText: 'ssh.input_label'.tr(), + ), + ), ), - ), + const SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => + context.read().trySubmit(), + text: 'ssh.create'.tr(), + ), + const SizedBox(height: 30), + ], ), - SizedBox(height: 30), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'ssh.create'.tr(), - ), - SizedBox(height: 30), - ], - ), + ), + ], ), - ], - ), - ); - }), - ), - ); - } + ); + }, + ), + ), + ); } diff --git a/lib/ui/pages/ssh_keys/ssh_keys.dart b/lib/ui/pages/ssh_keys/ssh_keys.dart index b67087c6..4059ba63 100644 --- a/lib/ui/pages/ssh_keys/ssh_keys.dart +++ b/lib/ui/pages/ssh_keys/ssh_keys.dart @@ -8,136 +8,137 @@ import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; -import '../../../config/brand_colors.dart'; -import '../../../config/brand_theme.dart'; -import '../../../logic/cubit/jobs/jobs_cubit.dart'; -import '../../../logic/models/user.dart'; -import '../../components/brand_button/brand_button.dart'; -import '../../components/brand_header/brand_header.dart'; +import 'package:selfprivacy/config/brand_colors.dart'; +import 'package:selfprivacy/config/brand_theme.dart'; +import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; +import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; part 'new_ssh_key.dart'; // Get user object as a parameter class SshKeysPage extends StatefulWidget { + const SshKeysPage({required this.user, final super.key}); final User user; - SshKeysPage({Key? key, required this.user}) : super(key: key); - @override - _SshKeysPageState createState() => _SshKeysPageState(); + State createState() => _SshKeysPageState(); } class _SshKeysPageState extends State { @override - Widget build(BuildContext context) { - return BrandHeroScreen( - heroTitle: 'ssh.title'.tr(), - heroSubtitle: widget.user.login, - heroIcon: BrandIcons.key, - children: [ - if (widget.user.login == 'root') - Column( - children: [ - // Show alert card if user is root - BrandCards.outlined( - child: ListTile( - leading: Icon( - Icons.warning_rounded, - color: Theme.of(context).colorScheme.error, + Widget build(final BuildContext context) => BrandHeroScreen( + heroTitle: 'ssh.title'.tr(), + heroSubtitle: widget.user.login, + heroIcon: BrandIcons.key, + children: [ + if (widget.user.login == 'root') + Column( + children: [ + // Show alert card if user is root + BrandCards.outlined( + child: ListTile( + leading: Icon( + Icons.warning_rounded, + color: Theme.of(context).colorScheme.error, + ), + title: Text('ssh.root.title'.tr()), + subtitle: Text('ssh.root.subtitle'.tr()), ), - title: Text('ssh.root.title'.tr()), - subtitle: Text('ssh.root.subtitle'.tr()), + ) + ], + ), + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'ssh.create'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + leading: const Icon(Icons.add_circle_outline_rounded), + onTap: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => Padding( + padding: MediaQuery.of(context).viewInsets, + child: _NewSshKey(widget.user), + ), + ); + }, ), - ) - ], - ), - BrandCards.outlined( - child: Column( - children: [ - ListTile( - title: Text( - 'ssh.create'.tr(), - style: Theme.of(context).textTheme.headline6, - ), - leading: Icon(Icons.add_circle_outline_rounded), - onTap: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: _NewSshKey(widget.user)); - }, - ); - }, - ), - Divider(height: 0), - // show a list of ListTiles with ssh keys - // Clicking on one should delete it - Column( - children: widget.user.sshKeys.map((key) { - final publicKey = - key.split(' ').length > 1 ? key.split(' ')[1] : key; - final keyType = key.split(' ')[0]; - final keyName = key.split(' ').length > 2 - ? key.split(' ')[2] - : 'ssh.no_key_name'.tr(); - return ListTile( + const Divider(height: 0), + // show a list of ListTiles with ssh keys + // Clicking on one should delete it + Column( + children: widget.user.sshKeys.map((final String key) { + final publicKey = + key.split(' ').length > 1 ? key.split(' ')[1] : key; + final keyType = key.split(' ')[0]; + final keyName = key.split(' ').length > 2 + ? key.split(' ')[2] + : 'ssh.no_key_name'.tr(); + return ListTile( title: Text('$keyName ($keyType)'), // do not overflow text - subtitle: Text(publicKey, - maxLines: 1, overflow: TextOverflow.ellipsis), + subtitle: Text( + publicKey, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), onTap: () { showDialog( context: context, - builder: (context) { - return AlertDialog( - title: Text('ssh.delete'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('ssh.delete_confirm_question'.tr()), - Text('$keyName ($keyType)'), - Text(publicKey), - ], - ), + builder: (final BuildContext context) => AlertDialog( + title: Text('ssh.delete'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text('ssh.delete_confirm_question'.tr()), + Text('$keyName ($keyType)'), + Text(publicKey), + ], ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context)..pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: TextStyle( - color: BrandColors.red1, - ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: const TextStyle( + color: BrandColors.red1, ), - onPressed: () { - context.read().addJob( - DeleteSSHKeyJob( - user: widget.user, publicKey: key)); - Navigator.of(context) - ..pop() - ..pop(); - }, ), - ], - ); - }, + onPressed: () { + context.read().addJob( + DeleteSSHKeyJob( + user: widget.user, + publicKey: key, + ), + ); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ), ); - }); - }).toList(), - ) - ], + }, + ); + }).toList(), + ) + ], + ), ), - ), - ], - ); - } + ], + ); } diff --git a/lib/ui/pages/users/add_user_fab.dart b/lib/ui/pages/users/add_user_fab.dart new file mode 100644 index 00000000..a78f056d --- /dev/null +++ b/lib/ui/pages/users/add_user_fab.dart @@ -0,0 +1,22 @@ +part of 'users.dart'; + +class AddUserFab extends StatelessWidget { + const AddUserFab({final super.key}); + + @override + Widget build(final BuildContext context) => FloatingActionButton.small( + heroTag: 'new_user_fab', + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (final BuildContext context) => Padding( + padding: MediaQuery.of(context).viewInsets, + child: const NewUser(), + ), + ); + }, + child: const Icon(Icons.person_add_outlined), + ); +} diff --git a/lib/ui/pages/users/empty.dart b/lib/ui/pages/users/empty.dart index e9623403..847003d3 100644 --- a/lib/ui/pages/users/empty.dart +++ b/lib/ui/pages/users/empty.dart @@ -1,35 +1,33 @@ part of 'users.dart'; class _NoUsers extends StatelessWidget { - const _NoUsers({Key? key, required this.text}) : super(key: key); + const _NoUsers({required this.text}); final String text; @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), - SizedBox(height: 20), - BrandText.h2( - 'users.nobody_here'.tr(), - style: TextStyle( - color: BrandColors.grey7, + Widget build(final BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(BrandIcons.users, size: 50, color: BrandColors.grey7), + const SizedBox(height: 20), + BrandText.h2( + 'users.nobody_here'.tr(), + style: const TextStyle( + color: BrandColors.grey7, + ), ), - ), - SizedBox(height: 10), - BrandText.medium( - text, - textAlign: TextAlign.center, - style: TextStyle( - color: BrandColors.grey7, + const SizedBox(height: 10), + BrandText.medium( + text, + textAlign: TextAlign.center, + style: const TextStyle( + color: BrandColors.grey7, + ), ), - ), - ], - ), - ); - } + ], + ), + ); } diff --git a/lib/ui/pages/users/fab.dart b/lib/ui/pages/users/fab.dart deleted file mode 100644 index d9a5a0ea..00000000 --- a/lib/ui/pages/users/fab.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'users.dart'; - -class _Fab extends StatelessWidget { - const _Fab({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - width: 48.0, - height: 48.0, - child: RawMaterialButton( - fillColor: BrandColors.blue, - shape: CircleBorder(), - elevation: 0.0, - highlightElevation: 2, - child: Icon( - Icons.add, - color: Colors.white, - size: 34, - ), - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: MediaQuery.of(context).viewInsets, - child: _NewUser()); - }, - ); - }, - ), - ); - } -} diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 58559c8f..72cb6387 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -1,99 +1,109 @@ part of 'users.dart'; -class _NewUser extends StatelessWidget { - @override - Widget build(BuildContext context) { - var config = context.watch().state; +class NewUser extends StatelessWidget { + const NewUser({final super.key}); - var domainName = UiHelpers.getDomainName(config); + @override + Widget build(final BuildContext context) { + final ServerInstallationState config = + context.watch().state; + + final String domainName = UiHelpers.getDomainName(config); return BrandBottomSheet( child: BlocProvider( - create: (context) { - var jobCubit = context.read(); - var jobState = jobCubit.state; - var users = []; + create: (final BuildContext context) { + final jobCubit = context.read(); + final jobState = jobCubit.state; + final users = []; users.addAll(context.read().state.users); if (jobState is JobsStateWithJobs) { - var jobs = jobState.jobList; - jobs.forEach((job) { + final jobs = jobState.jobList; + for (final job in jobs) { if (job is CreateUserJob) { users.add(job.user); } - }); + } } return UserFormCubit( jobsCubit: jobCubit, fieldFactory: FieldCubitFactory(context), ); }, - child: Builder(builder: (context) { - var formCubitState = context.watch().state; + child: Builder( + builder: (final BuildContext context) { + final FormCubitState formCubitState = + context.watch().state; - return BlocListener( - listener: (context, state) { - if (state.isSubmitted) { - Navigator.pop(context); - } - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - BrandHeader( - title: 'users.new_user'.tr(), - ), - SizedBox(width: 14), - Padding( - padding: paddingH15V0, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: CubitFormTextField( - formFieldCubit: context.read().login, - decoration: InputDecoration( - labelText: 'users.login'.tr(), - suffixText: '@$domainName', - ), - ), - ), - SizedBox(height: 20), - CubitFormTextField( - formFieldCubit: context.read().password, - decoration: InputDecoration( - alignLabelWithHint: false, - labelText: 'basis.password'.tr(), - suffixIcon: Padding( - padding: const EdgeInsets.only(right: 8), - child: IconButton( - icon: Icon( - BrandIcons.refresh, - color: BrandColors.blue, - ), - onPressed: - context.read().genNewPassword, + return BlocListener( + listener: + (final BuildContext context, final FormCubitState state) { + if (state.isSubmitted) { + Navigator.pop(context); + } + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + BrandHeader( + title: 'users.new_user'.tr(), + ), + const SizedBox(width: 14), + Padding( + padding: paddingH15V0, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicHeight( + child: CubitFormTextField( + formFieldCubit: context.read().login, + decoration: InputDecoration( + labelText: 'users.login'.tr(), + suffixText: '@$domainName', ), ), ), - ), - SizedBox(height: 30), - BrandButton.rised( - onPressed: formCubitState.isSubmitting - ? null - : () => context.read().trySubmit(), - text: 'basis.create'.tr(), - ), - SizedBox(height: 40), - Text('users.new_user_info_note'.tr()), - SizedBox(height: 30), - ], + const SizedBox(height: 20), + CubitFormTextField( + formFieldCubit: + context.read().password, + decoration: InputDecoration( + alignLabelWithHint: false, + labelText: 'basis.password'.tr(), + suffixIcon: Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: Icon( + BrandIcons.refresh, + color: + Theme.of(context).colorScheme.secondary, + ), + onPressed: context + .read() + .genNewPassword, + ), + ), + ), + ), + const SizedBox(height: 30), + BrandButton.rised( + onPressed: formCubitState.isSubmitting + ? null + : () => context.read().trySubmit(), + text: 'basis.create'.tr(), + ), + const SizedBox(height: 40), + Text('users.new_user_info_note'.tr()), + const SizedBox(height: 30), + ], + ), ), - ), - ], - ), - ); - }), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/ui/pages/users/user.dart b/lib/ui/pages/users/user.dart index a748a374..69a2e5dc 100644 --- a/lib/ui/pages/users/user.dart +++ b/lib/ui/pages/users/user.dart @@ -1,48 +1,51 @@ part of 'users.dart'; class _User extends StatelessWidget { - const _User({Key? key, required this.user, required this.isRootUser}) - : super(key: key); + const _User({ + required this.user, + required this.isRootUser, + }); final User user; final bool isRootUser; @override - Widget build(BuildContext context) { - return InkWell( - onTap: () { - showBrandBottomSheet( - context: context, - builder: (BuildContext context) { - return _UserDetails(user: user, isRootUser: isRootUser); - }, - ); - }, - child: Container( - padding: paddingH15V0, - height: 48, - child: Row( - children: [ - Container( - width: 17, - height: 17, - decoration: BoxDecoration( - color: user.color, - shape: BoxShape.circle, + Widget build(final BuildContext context) => InkWell( + onTap: () { + showBrandBottomSheet( + context: context, + builder: (final BuildContext context) => + _UserDetails(user: user, isRootUser: isRootUser), + ); + }, + child: Container( + padding: paddingH15V0, + height: 48, + child: Row( + children: [ + Container( + width: 17, + height: 17, + decoration: BoxDecoration( + color: user.color, + shape: BoxShape.circle, + ), ), - ), - SizedBox(width: 20), - Flexible( - child: isRootUser - ? BrandText.h4Underlined(user.login) - // cross out text if user not found on server - : BrandText.h4(user.login, - style: user.isFoundOnServer - ? null - : TextStyle(decoration: TextDecoration.lineThrough)), - ), - ], + const SizedBox(width: 20), + Flexible( + child: isRootUser + ? BrandText.h4Underlined(user.login) + // cross out text if user not found on server + : BrandText.h4( + user.login, + style: user.isFoundOnServer + ? null + : const TextStyle( + decoration: TextDecoration.lineThrough, + ), + ), + ), + ], + ), ), - ), - ); - } + ); } diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index a40a7b9a..d758c1f4 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -2,18 +2,18 @@ part of 'users.dart'; class _UserDetails extends StatelessWidget { const _UserDetails({ - Key? key, required this.user, required this.isRootUser, - }) : super(key: key); + }); final User user; final bool isRootUser; @override - Widget build(BuildContext context) { - var config = context.watch().state; + Widget build(final BuildContext context) { + final ServerInstallationState config = + context.watch().state; - var domainName = UiHelpers.getDomainName(config); + final String domainName = UiHelpers.getDomainName(config); return BrandBottomSheet( isExpended: true, @@ -25,7 +25,7 @@ class _UserDetails extends StatelessWidget { height: 200, decoration: BoxDecoration( color: user.color, - borderRadius: BorderRadius.vertical( + borderRadius: const BorderRadius.vertical( top: Radius.circular(20), ), ), @@ -36,7 +36,7 @@ class _UserDetails extends StatelessWidget { Align( alignment: Alignment.centerRight, child: Padding( - padding: EdgeInsets.symmetric( + padding: const EdgeInsets.symmetric( vertical: 4, horizontal: 2, ), @@ -44,67 +44,61 @@ class _UserDetails extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10.0), ), - onSelected: (PopupMenuItemType result) { + onSelected: (final PopupMenuItemType result) { switch (result) { case PopupMenuItemType.delete: showDialog( context: context, - builder: (context) { - return AlertDialog( - title: Text('basis.confirmation'.tr()), - content: SingleChildScrollView( - child: ListBody( - children: [ - Text('users.delete_confirm_question' - .tr()), - ], - ), - ), - actions: [ - TextButton( - child: Text('basis.cancel'.tr()), - onPressed: () { - Navigator.of(context)..pop(); - }, - ), - TextButton( - child: Text( - 'basis.delete'.tr(), - style: TextStyle( - color: BrandColors.red1, - ), + builder: (final BuildContext context) => + AlertDialog( + title: Text('basis.confirmation'.tr()), + content: SingleChildScrollView( + child: ListBody( + children: [ + Text( + 'users.delete_confirm_question'.tr(), + ), + ], + ), + ), + actions: [ + TextButton( + child: Text('basis.cancel'.tr()), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: Text( + 'basis.delete'.tr(), + style: const TextStyle( + color: BrandColors.red1, ), - onPressed: () { - context.read().addJob( - DeleteUserJob(user: user)); - Navigator.of(context) - ..pop() - ..pop(); - }, ), - ], - ); - }, + onPressed: () { + context + .read() + .addJob(DeleteUserJob(user: user)); + Navigator.of(context) + ..pop() + ..pop(); + }, + ), + ], + ), ); break; } }, - icon: Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - // PopupMenuItem( - // value: PopupMenuItemType.reset, - // child: Container( - // padding: EdgeInsets.only(left: 5), - // child: Text('users.reset_password'.tr()), - // ), - // ), + icon: const Icon(Icons.more_vert), + itemBuilder: (final BuildContext context) => [ PopupMenuItem( value: PopupMenuItemType.delete, child: Container( - padding: EdgeInsets.only(left: 5), + padding: const EdgeInsets.only(left: 5), child: Text( 'basis.delete'.tr(), - style: TextStyle(color: BrandColors.red1), + style: const TextStyle(color: BrandColors.red1), ), ), ), @@ -112,24 +106,25 @@ class _UserDetails extends StatelessWidget { ), ), ), - Spacer(), + const Spacer(), Padding( - padding: EdgeInsets.symmetric( - vertical: 20, - horizontal: 15, - ), - child: AutoSizeText( - user.login, - style: headline1Style, - softWrap: true, - minFontSize: 9, - maxLines: 3, - overflow: TextOverflow.ellipsis, - )), + padding: const EdgeInsets.symmetric( + vertical: 20, + horizontal: 15, + ), + child: AutoSizeText( + user.login, + style: headline1Style, + softWrap: true, + minFontSize: 9, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), ], ), ), - SizedBox(height: 20), + const SizedBox(height: 20), Padding( padding: paddingH15V0.copyWith(bottom: 20), child: Column( @@ -145,7 +140,7 @@ class _UserDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 14), + const SizedBox(height: 14), BrandText.small('basis.password'.tr()), Container( height: 40, @@ -154,30 +149,34 @@ class _UserDetails extends StatelessWidget { ), ], ), - SizedBox(height: 24), - BrandDivider(), - SizedBox(height: 20), + const SizedBox(height: 24), + const BrandDivider(), + const SizedBox(height: 20), ListTile( - onTap: () { - Navigator.of(context) - .push(materialRoute(SshKeysPage(user: user))); - }, - title: Text('ssh.title'.tr()), - subtitle: user.sshKeys.length > 0 - ? Text('ssh.subtitle_with_keys' - .tr(args: [user.sshKeys.length.toString()])) - : Text('ssh.subtitle_without_keys'.tr()), - trailing: Icon(BrandIcons.key)), - SizedBox(height: 20), + onTap: () { + Navigator.of(context) + .push(materialRoute(SshKeysPage(user: user))); + }, + title: Text('ssh.title'.tr()), + subtitle: user.sshKeys.isNotEmpty + ? Text( + 'ssh.subtitle_with_keys' + .tr(args: [user.sshKeys.length.toString()]), + ) + : Text('ssh.subtitle_without_keys'.tr()), + trailing: const Icon(BrandIcons.key), + ), + const SizedBox(height: 20), ListTile( onTap: () { Share.share( - 'login: ${user.login}, password: ${user.password}'); + 'login: ${user.login}, password: ${user.password}', + ); }, title: Text( 'users.send_registration_data'.tr(), ), - trailing: Icon(BrandIcons.share), + trailing: const Icon(BrandIcons.share), ), ], ), diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 013f65b0..659453d1 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -5,13 +5,13 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_colors.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/config/text_themes.dart'; -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/factories/field_cubit_factory.dart'; import 'package:selfprivacy/logic/cubit/forms/user/user_form_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/users/users_cubit.dart'; import 'package:selfprivacy/logic/models/job.dart'; -import 'package:selfprivacy/logic/models/user.dart'; +import 'package:selfprivacy/logic/models/hive/user.dart'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; @@ -24,31 +24,28 @@ import 'package:selfprivacy/ui/pages/ssh_keys/ssh_keys.dart'; import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:share_plus/share_plus.dart'; -import '../../../utils/route_transitions/basic.dart'; +import 'package:selfprivacy/utils/route_transitions/basic.dart'; part 'empty.dart'; -part 'fab.dart'; part 'new_user.dart'; part 'user.dart'; part 'user_details.dart'; +part 'add_user_fab.dart'; class UsersPage extends StatelessWidget { - const UsersPage({Key? key}) : super(key: key); + const UsersPage({final super.key}); @override - Widget build(BuildContext context) { - // final usersCubitState = context.watch().state; - var isReady = context.watch().state is AppConfigFinished; - // final primaryUser = usersCubitState.primaryUser; - // final users = [primaryUser, ...usersCubitState.users]; - // final isEmpty = users.isEmpty; + Widget build(final BuildContext context) { + final bool isReady = context.watch().state + is ServerInstallationFinished; Widget child; if (!isReady) { child = isNotReady(); } else { child = BlocBuilder( - builder: (context, state) { + builder: (final BuildContext context, final UsersState state) { print('Rebuild users page'); final primaryUser = state.primaryUser; final users = [primaryUser, ...state.users]; @@ -59,12 +56,11 @@ class UsersPage extends StatelessWidget { }, child: ListView.builder( itemCount: users.length, - itemBuilder: (BuildContext context, int index) { - return _User( - user: users[index], - isRootUser: index == 0, - ); - }, + itemBuilder: (final BuildContext context, final int index) => + _User( + user: users[index], + isRootUser: index == 0, + ), ), ); }, @@ -73,36 +69,32 @@ class UsersPage extends StatelessWidget { return Scaffold( appBar: PreferredSize( + preferredSize: const Size.fromHeight(52), child: BrandHeader( title: 'basis.users'.tr(), - hasFlashButton: true, ), - preferredSize: Size.fromHeight(52), ), - floatingActionButton: isReady ? _Fab() : null, body: child, ); } - Widget isNotReady() { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: NotReadyCard(), - ), - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Center( - child: _NoUsers( - text: 'users.not_ready'.tr(), + Widget isNotReady() => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.symmetric(horizontal: 15), + child: NotReadyCard(), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Center( + child: _NoUsers( + text: 'users.not_ready'.tr(), + ), ), ), - ), - ) - ], - ); - } + ) + ], + ); } diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart index 6785c171..9fcf7397 100644 --- a/lib/utils/color_utils.dart +++ b/lib/utils/color_utils.dart @@ -1,16 +1,17 @@ import 'package:flutter/material.dart'; -Color stringToColor(String string) { - var number = string.codeUnits.reduce((a, b) => a + b); - var index = number % colorPalette.length; +Color stringToColor(final String string) { + final int number = + string.codeUnits.reduce((final int a, final int b) => a + b); + final int index = number % colorPalette.length; return colorPalette[index]; } -var originalColor = Color(0xFFDBD8BD); +var originalColor = const Color(0xFFDBD8BD); var count = 40; var colorPalette = List.generate( count, - (index) => HSLColor.fromColor(originalColor) - .withHue((index) * 360.0 / count) + (final int index) => HSLColor.fromColor(originalColor) + .withHue(index * 360.0 / count) .toColor(), ); diff --git a/lib/utils/extensions/duration.dart b/lib/utils/extensions/duration.dart index a81627c0..2c302fb8 100644 --- a/lib/utils/extensions/duration.dart +++ b/lib/utils/extensions/duration.dart @@ -1,41 +1,32 @@ // ignore_for_file: unnecessary_this extension DurationFormatter on Duration { - String toDayHourMinuteSecondFormat() { - return [ - this.inHours.remainder(24), - this.inMinutes.remainder(60), - this.inSeconds.remainder(60) - ].map((seg) { - return seg.toString().padLeft(2, '0'); - }).join(':'); - } + String toDayHourMinuteSecondFormat() => [ + this.inHours.remainder(24), + this.inMinutes.remainder(60), + this.inSeconds.remainder(60) + ].map((final int seg) => seg.toString().padLeft(2, '0')).join(':'); String toDayHourMinuteFormat() { - var designator = this >= Duration.zero ? '+' : '-'; + final designator = this >= Duration.zero ? '+' : '-'; - var segments = [ + final Iterable segments = [ this.inHours.remainder(24).abs(), this.inMinutes.remainder(60).abs(), - ].map((seg) { - return seg.toString().padLeft(2, '0'); - }); + ].map((final int seg) => seg.toString().padLeft(2, '0')); return '$designator${segments.first}:${segments.last}'; } - String toHoursMinutesSecondsFormat() { - // WAT: https://flutterigniter.com/how-to-format-duration/ - return this.toString().split('.').first.padLeft(8, "0"); - } +// WAT: https://flutterigniter.com/how-to-format-duration/ + String toHoursMinutesSecondsFormat() => + this.toString().split('.').first.padLeft(8, '0'); String toDayHourMinuteFormat2() { - var segments = [ + final Iterable segments = [ this.inHours.remainder(24), this.inMinutes.remainder(60), - ].map((seg) { - return seg.toString().padLeft(2, '0'); - }); - return segments.first + " h" + " " + segments.last + " min"; + ].map((final int seg) => seg.toString().padLeft(2, '0')); + return '${segments.first} h ${segments.last} min'; } } diff --git a/lib/utils/extensions/elevation_extension.dart b/lib/utils/extensions/elevation_extension.dart index 78de82f1..9c6bbc14 100644 --- a/lib/utils/extensions/elevation_extension.dart +++ b/lib/utils/extensions/elevation_extension.dart @@ -5,26 +5,28 @@ import 'package:flutter/cupertino.dart'; extension ElevationExtension on BoxDecoration { BoxDecoration copyWith({ - Color? color, - DecorationImage? image, - BoxBorder? border, - BorderRadiusGeometry? borderRadius, - List? boxShadow, - Gradient? gradient, - BlendMode? backgroundBlendMode, - BoxShape? shape, - }) { - return BoxDecoration( - color: color ?? this.color, - image: image ?? this.image, - border: border ?? this.border, - borderRadius: borderRadius ?? this.borderRadius, - boxShadow: this.boxShadow != null || boxShadow != null - ? [...this.boxShadow ?? [], ...boxShadow ?? []] - : null, - gradient: gradient ?? this.gradient, - backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode, - shape: shape ?? this.shape, - ); - } + final Color? color, + final DecorationImage? image, + final BoxBorder? border, + final BorderRadiusGeometry? borderRadius, + final List? boxShadow, + final Gradient? gradient, + final BlendMode? backgroundBlendMode, + final BoxShape? shape, + }) => + BoxDecoration( + color: color ?? this.color, + image: image ?? this.image, + border: border ?? this.border, + borderRadius: borderRadius ?? this.borderRadius, + boxShadow: this.boxShadow != null || boxShadow != null + ? [ + ...this.boxShadow ?? [], + ...boxShadow ?? [] + ] + : null, + gradient: gradient ?? this.gradient, + backgroundBlendMode: backgroundBlendMode ?? this.backgroundBlendMode, + shape: shape ?? this.shape, + ); } diff --git a/lib/utils/extensions/text_extensions.dart b/lib/utils/extensions/text_extensions.dart index 26932a11..bfacc600 100644 --- a/lib/utils/extensions/text_extensions.dart +++ b/lib/utils/extensions/text_extensions.dart @@ -1,39 +1,40 @@ import 'package:flutter/cupertino.dart'; extension TextExtension on Text { - Text withColor(Color color) => Text( + Text withColor(final Color color) => Text( data!, - key: this.key, - strutStyle: this.strutStyle, - textAlign: this.textAlign, - textDirection: this.textDirection, - locale: this.locale, - softWrap: this.softWrap, - overflow: this.overflow, - textScaleFactor: this.textScaleFactor, - maxLines: this.maxLines, - semanticsLabel: this.semanticsLabel, - textWidthBasis: textWidthBasis ?? this.textWidthBasis, - style: this.style != null - ? this.style!.copyWith(color: color) + key: key, + strutStyle: strutStyle, + textAlign: textAlign, + textDirection: textDirection, + locale: locale, + softWrap: softWrap, + overflow: overflow, + textScaleFactor: textScaleFactor, + maxLines: maxLines, + semanticsLabel: semanticsLabel, + textWidthBasis: textWidthBasis ?? textWidthBasis, + style: style != null + ? style!.copyWith(color: color) : TextStyle(color: color), ); Text copyWith({ - Key? key, - StrutStyle? strutStyle, - TextAlign? textAlign, - TextDirection? textDirection, - Locale? locale, - bool? softWrap, - TextOverflow? overflow, - double? textScaleFactor, - int? maxLines, - String? semanticsLabel, - TextWidthBasis? textWidthBasis, - TextStyle? style, - }) { - return Text(data!, + final Key? key, + final StrutStyle? strutStyle, + final TextAlign? textAlign, + final TextDirection? textDirection, + final Locale? locale, + final bool? softWrap, + final TextOverflow? overflow, + final double? textScaleFactor, + final int? maxLines, + final String? semanticsLabel, + final TextWidthBasis? textWidthBasis, + final TextStyle? style, + }) => + Text( + data!, key: key ?? this.key, strutStyle: strutStyle ?? this.strutStyle, textAlign: textAlign ?? this.textAlign, @@ -45,6 +46,6 @@ extension TextExtension on Text { maxLines: maxLines ?? this.maxLines, semanticsLabel: semanticsLabel ?? this.semanticsLabel, textWidthBasis: textWidthBasis ?? this.textWidthBasis, - style: style != null ? this.style?.merge(style) ?? style : this.style); - } + style: style != null ? this.style?.merge(style) ?? style : this.style, + ); } diff --git a/lib/utils/password_generator.dart b/lib/utils/password_generator.dart index 35bdaecb..5acf3888 100644 --- a/lib/utils/password_generator.dart +++ b/lib/utils/password_generator.dart @@ -5,23 +5,35 @@ Random _rnd = Random(); typedef StringGeneratorFunction = String Function(); class StringGenerators { - static const letters = 'abcdefghijklmnopqrstuvwxyz'; - static const numbers = '1234567890'; - static const symbols = '_'; + static const String letters = 'abcdefghijklmnopqrstuvwxyz'; + static const String numbers = '1234567890'; + static const String symbols = '_'; static String getRandomString( - int length, { - hasLowercaseLetters = false, - hasUppercaseLetters = false, - hasNumbers = false, - hasSymbols = false, - isStrict = false, + final int length, { + final hasLowercaseLetters = false, + final hasUppercaseLetters = false, + final hasNumbers = false, + final hasSymbols = false, + final isStrict = false, }) { - var chars = ''; - if (hasLowercaseLetters) chars += letters; - if (hasUppercaseLetters) chars += letters.toUpperCase(); - if (hasNumbers) chars += numbers; - if (hasSymbols) chars += symbols; + String chars = ''; + + if (hasLowercaseLetters) { + chars += letters; + } + + if (hasUppercaseLetters) { + chars += letters.toUpperCase(); + } + + if (hasNumbers) { + chars += numbers; + } + + if (hasSymbols) { + chars += symbols; + } assert(chars.isNotEmpty, 'chart empty'); @@ -29,8 +41,8 @@ class StringGenerators { return genString(length, chars); } - var res = ''; - var loose = length; + String res = ''; + int loose = length; if (hasLowercaseLetters) { loose -= 1; res += genString(1, letters); @@ -49,20 +61,19 @@ class StringGenerators { } res += genString(loose, chars); - var shuffledlist = res.split('')..shuffle(); + final List shuffledlist = res.split('')..shuffle(); return shuffledlist.join(); } - static String genString(int length, String chars) { - return String.fromCharCodes( - Iterable.generate( - length, - (_) => chars.codeUnitAt( - _rnd.nextInt(chars.length), + static String genString(final int length, final String chars) => + String.fromCharCodes( + Iterable.generate( + length, + (final _) => chars.codeUnitAt( + _rnd.nextInt(chars.length), + ), ), - ), - ); - } + ); static StringGeneratorFunction userPassword = () => getRandomString( 8, diff --git a/lib/utils/route_transitions/basic.dart b/lib/utils/route_transitions/basic.dart index 2cea3eb9..a3148e1d 100644 --- a/lib/utils/route_transitions/basic.dart +++ b/lib/utils/route_transitions/basic.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; -Route materialRoute(Widget widget) => MaterialPageRoute( - builder: (context) => widget, +Route materialRoute(final Widget widget) => MaterialPageRoute( + builder: (final BuildContext context) => widget, ); -Route noAnimationRoute(Widget widget) => PageRouteBuilder( - pageBuilder: (context, animation1, animation2) => widget, +Route noAnimationRoute(final Widget widget) => PageRouteBuilder( + pageBuilder: ( + final BuildContext context, + final Animation animation1, + final Animation animation2, + ) => + widget, ); diff --git a/lib/utils/route_transitions/slide_bottom.dart b/lib/utils/route_transitions/slide_bottom.dart index 380b1142..e187b4d7 100644 --- a/lib/utils/route_transitions/slide_bottom.dart +++ b/lib/utils/route_transitions/slide_bottom.dart @@ -1,45 +1,48 @@ import 'package:flutter/material.dart'; -Function pageBuilder = (Widget widget) => ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, +Function pageBuilder = (final Widget widget) => ( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, ) => widget; Function transitionsBuilder = ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -) { - return SlideTransition( - position: Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(animation), - child: Container( - decoration: animation.isCompleted - ? null - : BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.black, + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, + final Widget child, +) => + SlideTransition( + position: Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(animation), + child: Container( + decoration: animation.isCompleted + ? null + : const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.black, + ), ), ), - ), - child: child, - ), - ); -}; + child: child, + ), + ); class SlideBottomRoute extends PageRouteBuilder { SlideBottomRoute(this.widget) : super( - transitionDuration: Duration(milliseconds: 150), + transitionDuration: const Duration(milliseconds: 150), pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( - BuildContext, Animation, Animation, Widget), + BuildContext, + Animation, + Animation, + Widget, + ), ); final Widget widget; diff --git a/lib/utils/route_transitions/slide_right.dart b/lib/utils/route_transitions/slide_right.dart index f01c4b0f..eae4414d 100644 --- a/lib/utils/route_transitions/slide_right.dart +++ b/lib/utils/route_transitions/slide_right.dart @@ -1,44 +1,47 @@ import 'package:flutter/material.dart'; -Function pageBuilder = (Widget widget) => ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, +Function pageBuilder = (final Widget widget) => ( + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, ) => widget; Function transitionsBuilder = ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, -) { - return SlideTransition( - position: Tween( - begin: const Offset(-1, 0), - end: Offset.zero, - ).animate(animation), - child: Container( - decoration: animation.isCompleted - ? null - : BoxDecoration( - border: Border( - right: BorderSide( - color: Colors.black, + final BuildContext context, + final Animation animation, + final Animation secondaryAnimation, + final Widget child, +) => + SlideTransition( + position: Tween( + begin: const Offset(-1, 0), + end: Offset.zero, + ).animate(animation), + child: Container( + decoration: animation.isCompleted + ? null + : const BoxDecoration( + border: Border( + right: BorderSide( + color: Colors.black, + ), ), ), - ), - child: child, - ), - ); -}; + child: child, + ), + ); class SlideRightRoute extends PageRouteBuilder { SlideRightRoute(this.widget) : super( pageBuilder: pageBuilder(widget), transitionsBuilder: transitionsBuilder as Widget Function( - BuildContext, Animation, Animation, Widget), + BuildContext, + Animation, + Animation, + Widget, + ), ); final Widget widget; diff --git a/lib/utils/ui_helpers.dart b/lib/utils/ui_helpers.dart index 9ce41d0c..c34721e1 100644 --- a/lib/utils/ui_helpers.dart +++ b/lib/utils/ui_helpers.dart @@ -1,9 +1,8 @@ -import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; /// it's ui helpers use only for ui components, don't use for logic components. class UiHelpers { - static String getDomainName(AppConfigState config) => config.isDomainFilled - ? config.cloudFlareDomain!.domainName - : 'example.com'; + static String getDomainName(final ServerInstallationState config) => + config.isDomainFilled ? config.serverDomain!.domainName : 'example.com'; } diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 00000000..cc332a28 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "selfprivacy") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "pro.kherel.selfprivacy") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..cf327b12 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_theme_fl_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkThemeFlPlugin"); + gtk_theme_fl_plugin_register_with_registrar(gtk_theme_fl_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..6c700a87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux + gtk_theme_fl + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/my_application.cc b/linux/my_application.cc new file mode 100644 index 00000000..8c470fbc --- /dev/null +++ b/linux/my_application.cc @@ -0,0 +1,104 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "selfprivacy"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "selfprivacy"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/my_application.h b/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pubspec.lock b/pubspec.lock index 681a805a..e3faf1b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,28 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "31.0.0" + version: "38.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "2.8.0" + version: "3.4.1" archive: dependency: transitive description: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "3.3.0" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "2.3.0" + version: "2.3.1" asn1lib: dependency: transitive description: @@ -56,14 +56,14 @@ packages: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "4.2.0" + version: "4.2.2" bloc: dependency: transitive description: name: bloc url: "https://pub.dartlang.org" source: hosted - version: "8.0.2" + version: "8.0.3" boolean_selector: dependency: transitive description: @@ -77,7 +77,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.0" build_config: dependency: transitive description: @@ -91,21 +91,21 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" + version: "2.1.11" build_runner_core: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "8.1.4" + version: "8.3.0" characters: dependency: transitive description: @@ -148,13 +148,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" clock: dependency: transitive description: @@ -175,7 +168,7 @@ packages: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.15.0" + version: "1.16.0" convert: dependency: transitive description: @@ -189,7 +182,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.3.1" crypt: dependency: "direct main" description: @@ -203,7 +196,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" cubit_form: dependency: "direct main" description: @@ -224,21 +217,70 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.3" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.3" + device_info_plus_linux: + dependency: transitive + description: + name: device_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0+1" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" dio: dependency: "direct main" description: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.4" + version: "4.0.6" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" easy_localization: dependency: "direct main" description: name: easy_localization url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" easy_logger: dependency: transitive description: @@ -273,14 +315,14 @@ packages: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" ffi: dependency: transitive description: name: ffi url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.2.1" file: dependency: transitive description: @@ -294,14 +336,14 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" fl_chart: dependency: "direct main" description: name: fl_chart url: "https://pub.dartlang.org" source: hosted - version: "0.50.1" + version: "0.50.5" flutter: dependency: "direct main" description: flutter @@ -321,6 +363,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" flutter_localizations: dependency: transitive description: flutter @@ -332,14 +381,14 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.9" + version: "0.6.10" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" flutter_secure_storage: dependency: "direct main" description: @@ -420,13 +469,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + gtk_theme_fl: + dependency: "direct main" + description: + name: gtk_theme_fl + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1" hive: dependency: "direct main" description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.0" hive_flutter: dependency: "direct main" description: @@ -468,9 +524,9 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.1.3" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" @@ -496,28 +552,56 @@ packages: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.3" + version: "0.6.4" json_annotation: dependency: "direct main" description: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "4.5.0" json_serializable: dependency: "direct dev" description: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "6.1.4" + version: "6.2.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" local_auth: dependency: "direct main" description: name: local_auth url: "https://pub.dartlang.org" source: hosted - version: "1.1.11" + version: "2.0.2" + local_auth_android: + dependency: transitive + description: + name: local_auth_android + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + local_auth_ios: + dependency: transitive + description: + name: local_auth_ios + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + local_auth_platform_interface: + dependency: transitive + description: + name: local_auth_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" logging: dependency: transitive description: @@ -531,14 +615,14 @@ packages: name: markdown url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "5.0.0" mask_text_input_formatter: dependency: transitive description: name: mask_text_input_formatter url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.3.0" matcher: dependency: transitive description: @@ -552,7 +636,7 @@ packages: name: material_color_utilities url: "https://pub.dartlang.org" source: hosted - version: "0.1.3" + version: "0.1.4" meta: dependency: transitive description: @@ -566,14 +650,14 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" modal_bottom_sheet: dependency: "direct main" description: name: modal_bottom_sheet url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" nanoid: dependency: "direct main" description: @@ -615,63 +699,63 @@ packages: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.8.0" + version: "1.8.1" path_provider: dependency: transitive description: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.10" path_provider_android: dependency: transitive description: name: path_provider_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.14" path_provider_ios: dependency: transitive description: name: path_provider_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.9" path_provider_linux: dependency: transitive description: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.6" petitparser: dependency: transitive description: name: petitparser url: "https://pub.dartlang.org" source: hosted - version: "4.4.0" + version: "5.0.0" platform: dependency: transitive description: @@ -687,12 +771,12 @@ packages: source: hosted version: "2.1.2" pointycastle: - dependency: "direct main" + dependency: transitive description: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.5.1" + version: "3.6.0" pool: dependency: transitive description: @@ -722,12 +806,12 @@ packages: source: hosted version: "6.0.2" pub_semver: - dependency: transitive + dependency: "direct main" description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" pubspec_parse: dependency: transitive description: @@ -741,91 +825,84 @@ packages: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "3.0.1+1" - rsa_encrypt: - dependency: "direct main" - description: - name: rsa_encrypt - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" + version: "3.1.0" share_plus: dependency: "direct main" description: name: share_plus url: "https://pub.dartlang.org" source: hosted - version: "3.0.5" + version: "4.0.4" share_plus_linux: dependency: transitive description: name: share_plus_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_macos: dependency: transitive description: name: share_plus_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.2" share_plus_web: dependency: transitive description: name: share_plus_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "3.0.0" share_plus_windows: dependency: transitive description: name: share_plus_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "3.0.0" shared_preferences: dependency: transitive description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.13" + version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: @@ -839,21 +916,21 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" shelf_packages_handler: dependency: transitive description: @@ -886,14 +963,14 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.2" source_helper: dependency: transitive description: name: source_helper url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.2" source_map_stack_trace: dependency: transitive description: @@ -914,7 +991,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.8.1" + version: "1.8.2" ssh_key: dependency: "direct main" description: @@ -950,6 +1027,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + system_theme: + dependency: "direct main" + description: + name: system_theme + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + system_theme_web: + dependency: transitive + description: + name: system_theme_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" term_glyph: dependency: transitive description: @@ -963,21 +1054,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.19.5" + version: "1.21.1" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.8" + version: "0.4.9" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.9" + version: "0.4.13" timezone: dependency: "direct main" description: @@ -1005,42 +1096,42 @@ packages: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.20" + version: "6.1.2" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.15" + version: "6.0.16" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: @@ -1054,35 +1145,35 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.11" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.1" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" vm_service: dependency: transitive description: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "7.5.0" + version: "8.3.0" wakelock: dependency: "direct main" description: name: wakelock url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" + version: "0.6.1+2" wakelock_macos: dependency: transitive description: @@ -1124,21 +1215,21 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.4.1" + version: "2.6.1" xdg_directories: dependency: transitive description: @@ -1152,14 +1243,14 @@ packages: name: xml url: "https://pub.dartlang.org" source: hosted - version: "5.3.1" + version: "5.4.1" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "3.1.0" + version: "3.1.1" sdks: - dart: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + dart: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 29a3100c..71a5f2f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: selfprivacy description: selfprivacy.org publish_to: 'none' -version: 0.5.3+14 +version: 0.6.0+15 environment: - sdk: '>=2.13.4 <3.0.0' - flutter: ">=2.10.0" + sdk: '>=2.17.0 <3.0.0' + flutter: ">=3.0.0" dependencies: auto_size_text: ^3.0.0 @@ -13,7 +13,9 @@ dependencies: crypt: ^4.2.1 cubit_form: ^2.0.1 cupertino_icons: ^1.0.4 + device_info_plus: ^3.2.3 dio: ^4.0.4 + dynamic_color: ^1.2.2 easy_localization: ^3.0.0 either_option: ^2.0.1-dev.1 equatable: ^2.0.3 @@ -24,20 +26,22 @@ dependencies: flutter_markdown: ^0.6.9 flutter_secure_storage: ^5.0.2 get_it: ^7.2.0 + gtk_theme_fl: ^0.0.1 hive: ^2.0.5 hive_flutter: ^1.1.0 + intl: ^0.17.0 ionicons: ^0.1.2 json_annotation: ^4.4.0 - local_auth: ^1.1.11 - modal_bottom_sheet: ^2.0.0 + local_auth: ^2.0.2 + modal_bottom_sheet: ^2.0.1 nanoid: ^1.0.0 package_info: ^2.0.2 - pointycastle: ^3.5.1 pretty_dio_logger: ^1.2.0-beta-1 provider: ^6.0.2 - rsa_encrypt: ^2.0.0 - share_plus: ^3.0.5 + pub_semver: ^2.1.1 + share_plus: ^4.0.4 ssh_key: ^0.7.1 + system_theme: ^2.0.0 timezone: ^0.8.0 url_launcher: ^6.0.20 wakelock: ^0.6.1+1 @@ -49,6 +53,7 @@ dev_dependencies: flutter_launcher_icons: ^0.9.2 hive_generator: ^1.0.0 json_serializable: ^6.1.4 + flutter_lints: ^2.0.1 flutter_icons: android: "launcher_icon" diff --git a/test/widget_test.dart b/test/widget_test.dart index aaa33419..b8799e1f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -6,14 +6,17 @@ void main() { group('StringGenerators', () { group('Basic', () { test('assert chart empty', () { - expect(() { - StringGenerators.getRandomString(8); - }, throwsAssertionError); + expect( + () { + StringGenerators.getRandomString(8); + }, + throwsAssertionError, + ); }); test('only lowercase string', () { - var length = 8; - var generatedString = + const int length = 8; + final String generatedString = StringGenerators.getRandomString(length, hasLowercaseLetters: true); expect(generatedString, isNot(matches(regExpNewLines))); @@ -26,9 +29,12 @@ void main() { }); test('only uppercase string', () { - var length = 8; - var generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: false, hasUppercaseLetters: true); + const int length = 8; + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: false, + hasUppercaseLetters: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -40,11 +46,13 @@ void main() { }); test('only numbers string', () { - var length = 8; - var generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: false, - hasUppercaseLetters: false, - hasNumbers: true); + const int length = 8; + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: false, + hasUppercaseLetters: false, + hasNumbers: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -56,8 +64,8 @@ void main() { }); test('only symbols string', () { - var length = 8; - var generatedString = StringGenerators.getRandomString( + const int length = 8; + final String generatedString = StringGenerators.getRandomString( length, hasLowercaseLetters: false, hasUppercaseLetters: false, @@ -77,13 +85,15 @@ void main() { group('Strict mode', () { test('All', () { - var length = 5; - var generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: true, - hasUppercaseLetters: true, - hasNumbers: true, - hasSymbols: true, - isStrict: true); + const int length = 5; + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: true, + hasUppercaseLetters: true, + hasNumbers: true, + hasSymbols: true, + isStrict: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -94,13 +104,15 @@ void main() { expect(generatedString.length, equals(length)); }); test('Lowercase letters and numbers', () { - var length = 3; - var generatedString = StringGenerators.getRandomString(length, - hasLowercaseLetters: true, - hasUppercaseLetters: false, - hasNumbers: true, - hasSymbols: false, - isStrict: true); + const int length = 3; + final String generatedString = StringGenerators.getRandomString( + length, + hasLowercaseLetters: true, + hasUppercaseLetters: false, + hasNumbers: true, + hasSymbols: false, + isStrict: true, + ); expect(generatedString, isNot(matches(regExpNewLines))); expect(generatedString, isNot(matches(regExpWhiteSpaces))); @@ -114,9 +126,9 @@ void main() { }); } -var regExpNewLines = RegExp(r"[\n\r]+"); -var regExpWhiteSpaces = RegExp(r"[\s]+"); -var regExpUppercaseLetters = RegExp(r"[A-Z]"); -var regExpLowercaseLetters = RegExp(r"[a-z]"); -var regExpNumbers = RegExp(r"[0-9]"); -var regExpSymbols = RegExp(r'(?:_|[^\w\s])+'); +RegExp regExpNewLines = RegExp(r'[\n\r]+'); +RegExp regExpWhiteSpaces = RegExp(r'[\s]+'); +RegExp regExpUppercaseLetters = RegExp(r'[A-Z]'); +RegExp regExpLowercaseLetters = RegExp(r'[a-z]'); +RegExp regExpNumbers = RegExp(r'[0-9]'); +RegExp regExpSymbols = RegExp(r'(?:_|[^\w\s])+'); diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 00000000..e3ec7b3a --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(selfprivacy LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "selfprivacy") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..930d2071 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..37b7695e --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + SystemThemePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemThemePlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..a1aaa278 --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,26 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows + system_theme + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..b9e550fb --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 00000000..e2d13c33 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "pro.kherel" "\0" + VALUE "FileDescription", "selfprivacy" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "selfprivacy" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 pro.kherel. All rights reserved." "\0" + VALUE "OriginalFilename", "selfprivacy.exe" "\0" + VALUE "ProductName", "selfprivacy" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 00000000..ce57a2c0 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"selfprivacy", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 00000000..f5bf9fa0 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 00000000..c10f08dc --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_