diff --git a/assets/translations/en.json b/assets/translations/en.json index dfd5e7f7..a93e7183 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -20,18 +20,28 @@ "domain": "Domain", "saving": "Saving..", "nickname": "nickname", - "loading": "loading" + "loading": "Loading...", + "later": "I will setup it later", + "reset": "Reset", + "details": "Details", + "no_data": "No data" }, "more": { "_comment": "'More' tab", "configuration_wizard": "Setup wizard", - "settings": "Application settings", "about_project": "About us", "about_app": "About application", "onboarding": "Onboarding", "console": "Console", "about_app_page": { "text": "Тут любая служебная информация, v.{}" + }, + "settings": { + "title": "Application settings", + "1": "Dark Theme", + "2": "Change your the app theme", + "3": "Reset app config", + "4": "Reset api keys and root user" } }, "onboarding": { @@ -46,26 +56,27 @@ "page_title": "Your Data Center", "server": { "card_title": "Server", + "status": "Status — Good", "bottom_sheet": { "1": "It's a virtual computer, where all your services live.", - "2": "1 CPU, RAM 4Gb, 40Gb — $5 per month", - "3": "Status — Good" + "2": "General information", + "3": "Location" } }, "domain": { "card_title": "Domain", + "status": "Status — Good", "bottom_sheet": { "1": "It's your personal internet address that will point to the server and other services of yours.", - "2": "{} — expires on {}", - "3": "Status — Good" + "2": "{} — expires on {}" } }, "backup": { "card_title": "Backup", + "status": "Status — Good", "bottom_sheet": { "1": "Will save your day in case of incident: hackers attack, server deletion, etc.", - "2": "3Gb/10Gb, last backup was yesterday {}", - "3": "Status — Good" + "2": "3Gb/10Gb, last backup was yesterday {}" } } }, @@ -122,7 +133,7 @@ } }, "git": { - "title": "Git-server", + "title": "Git Server", "subtitle": "Private alternative to the Github, that belongs to you, but not a Microsoft.", "bottom_sheet": { "1": "You can connect and create a new user here:" @@ -147,7 +158,7 @@ "initializing": { "_comment": "initializing page", "1": "Connect Hetzner server", - "2": "Here, your data and SelfPrivacy services wiil reside", + "2": "Here, your data and SelfPrivacy services wiil reside", "how": "How to obtain API token", "3": "Connect CloudFlare", "4": "To manage your domain's DNS", @@ -157,6 +168,7 @@ "8": "Loading domains list", "9": "Found more than one domain. For your own security, please be asked to delete unnecessary domains", "10": "Save domain", + "final": "Final step", "11": "Create server", "what": "What does it mean?", "13": "Server rebooted. Waiting for the last verification...", @@ -166,6 +178,18 @@ "17": "Check", "18": "How to obtain Hetzner API Token", "19": "1 Go via this link ", - "20": "\n" + "20": "\n", + "21": "One more restart to apply your security certificates." + }, + "modals": { + "_comment": "messages in modals", + "1": "Server with such name, already exist", + "2": "Destroy server and create a new one?", + "3": "Are you sure?", + "4": "Purge all authentication keys?", + "5": "Yes, purge all my tokens" + }, + "timer": { + "sec": "{} sec" } } \ No newline at end of file diff --git a/assets/translations/ru.json b/assets/translations/ru.json index d3b19c1b..df845f70 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -20,18 +20,28 @@ "domain": "Домен", "saving": "Сохранение..", "nickname": "Никнейм", - "loading": "Загрузка" + "loading": "Загрузка", + "later": "Настрою потом", + "reset": "Reset", + "details": "Детальная информация", + "no_data": "Нет данных" }, "more": { "_comment": "вкладка еще", "configuration_wizard": "Мастер Подключения", - "settings": "Настройки приложения", "about_project": "О проекте SelfPrivacy", "about_app": "О приложении", - "onboarding": "Onboarding", - "console": "Console", + "onboarding": "Приветствие", + "console": "Консоль", "about_app_page": { - "text": "Тут любая служебная информация, v.{}" + "text": "Версия приложения: v.{}" + }, + "settings": { + "title": "Настройки приложения", + "1": "Темная тема", + "2": "Сменить цветовую тему", + "3": "Сброс настроек", + "4": "Сбросить API ключи а так же root пользвателя" } }, "onboarding": { @@ -46,26 +56,27 @@ "page_title": "Ваш Дата-центр", "server": { "card_title": "Сервер", + "status": "Статус — в норме", "bottom_sheet": { "1": "Это виртульный компьютер на котором работают все ваши сервисы.", - "2": "1 CPU, RAM 4Gb, 40Gb — $5 в месяц", - "3": "Статус — в норме" + "2": "Общая информация", + "3": "Размещение" } }, "domain": { "card_title": "Домен", + "status": "Статус — в норме", "bottom_sheet": { "1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы.", - "2": "{} — продлен до {}", - "3": "Статус — в норме" + "2": "{} — продлен до {}" } }, "backup": { "card_title": "Резервное копирование", + "status": "Статус — в норме", "bottom_sheet": { "1": "Выручит в любой ситуации: хакерская атака, удаление сервера и т.п.", - "2": "3Gb — бестплатно до 10Gb, последний вчера в {}", - "3": "Статус — в норме" + "2": "3Gb — бестплатно до 10Gb, последний вчера в {}" } } }, @@ -136,8 +147,8 @@ "not_ready": "Подключите сервер, домен и DNS в разделу Провайдеры, чтобы добавить первого пользователя", "nobody_here": "'Здесь пока никого'", "login": "Логин", - "onboarding": "Onboarding", - "console": "Console", + "onboarding": "Приветствие", + "console": "Консоль разработчика", "new_user_info_note": "Новый пользователь автоматически получит доступ ко всем сервисам. Ещё какое-то описание.", "delete_confirm_question": "удалить учетную запись?", "reset_password": "Сбросить пароль", @@ -157,6 +168,7 @@ "8": "Загружаем список доменов", "9": "Найдено больше одного домена, для вашей безопастности, просим вам удалить не нужные домены", "10": "Сохранить домен", + "final": "Последний шаг", "11": "Создать сервер", "what": "Что это значит?", "13": "Сервер презагружен, ждем последнюю проверку", @@ -166,6 +178,18 @@ "17": "Проверка", "18": "Как получить Hetzner API Token'", "19": "1 Переходим по ссылке ", - "20": "\n2 Заходим в созданный нами проект. Если такового - нет, значит создаём.\n3 Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).\n4 Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.\n5 В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же вы используете мобильную версию сайта, в нижнем правом углу вы увидите красный плюсик. Нажимаем на эту кнопку.\n6 В поле Description, даём нашему токену название (это может быть любое название, которые вам нравиться. Сути оно не меняет." + "20": "\n2 Заходим в созданный нами проект. Если такового - нет, значит создаём.\n3 Наводим мышкой на боковую панель. Она должна раскрыться, показав нам пункты меню. Нас интересует последний — Security (с иконкой ключика).\n4 Далее, в верхней части интерфейса видим примерно такой список: SSH Keys, API Tokens, Certificates, Members. Нам нужен API Tokens. Переходим по нему.\n5 В правой части интерфейса, нас будет ожидать кнопка Generate API token. Если же вы используете мобильную версию сайта, в нижнем правом углу вы увидите красный плюсик. Нажимаем на эту кнопку.\n6 В поле Description, даём нашему токену название (это может быть любое название, которые вам нравиться. Сути оно не меняет.", + "21": "Сейчас будет дополнительная перезагрузка для активации сертификатов безопастности" + }, + "modals": { + "_comment": "messages in modals", + "1": "Сервер с таким именем уже существует", + "2": "Уничтожить сервер и создать новый?", + "3": "Вы уверенны", + "4": "Сбросить все ключи?", + "5": "Да, сбросить" + }, + "timer": { + "sec": "{} сек" } } \ No newline at end of file diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..709b623e --- /dev/null +++ b/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + create_factory: true + create_to_json: false \ No newline at end of file diff --git a/lib/config/get_it_config.dart b/lib/config/get_it_config.dart index 6a05afc4..3f01e6d3 100644 --- a/lib/config/get_it_config.dart +++ b/lib/config/get_it_config.dart @@ -1,17 +1,23 @@ 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/timer.dart'; +export 'package:selfprivacy/logic/get_it/api_config.dart'; export 'package:selfprivacy/logic/get_it/console.dart'; export 'package:selfprivacy/logic/get_it/navigation.dart'; export 'package:selfprivacy/logic/get_it/timer.dart'; + final getIt = GetIt.instance; -void getItSetup() { +Future getItSetup() async { getIt.registerSingleton(NavigationService()); getIt.registerSingleton(ConsoleModel()); getIt.registerSingleton(TimerModel()); + getIt.registerSingleton(ApiConfigModel()..init()); + + await getIt.allReady(); } diff --git a/lib/config/hive_config.dart b/lib/config/hive_config.dart index 17a10fb3..eaf7b0eb 100644 --- a/lib/config/hive_config.dart +++ b/lib/config/hive_config.dart @@ -16,6 +16,7 @@ class HiveConfig { Hive.registerAdapter(HetznerServerDetailsAdapter()); Hive.registerAdapter(CloudFlareDomainAdapter()); Hive.registerAdapter(BackblazeCredentialAdapter()); + Hive.registerAdapter(HetznerDataBaseAdapter()); await Hive.openBox(BNames.appSettings); var cipher = HiveAesCipher(await getEncriptedKey()); @@ -55,5 +56,6 @@ class BNames { static String isServerStarted = 'isServerStarted'; static String backblazeKey = 'backblazeKey'; static String isLoading = 'isLoading'; - static String isServerReseted = 'isServerReseted'; + static String isServerResetedFirstTime = 'isServerResetedFirstTime'; + static String isServerResetedSecondTime = 'isServerResetedSecondTime'; } diff --git a/lib/config/text_themes.dart b/lib/config/text_themes.dart index b64ab1a2..c84aeaed 100644 --- a/lib/config/text_themes.dart +++ b/lib/config/text_themes.dart @@ -38,6 +38,12 @@ final headline4Style = defaultTextStyle.copyWith( color: BrandColors.headlineColor, ); +final headline5Style = defaultTextStyle.copyWith( + fontSize: 15, + fontWeight: NamedFontWeight.medium, + color: BrandColors.headlineColor.withOpacity(0.8), +); + final body1Style = defaultTextStyle; final body2Style = defaultTextStyle.copyWith( color: BrandColors.textColor2, @@ -59,9 +65,9 @@ final linkStyle = defaultTextStyle.copyWith(color: BrandColors.blue); final progressTextStyleLight = TextStyle( fontSize: 11, color: BrandColors.textColor1, + height: 1.7, ); -final progressTextStyleDark = TextStyle( - fontSize: 11, +final 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 505ec178..5f4330d4 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -1,29 +1,41 @@ +import 'dart:async'; import 'dart:developer'; import 'dart:io'; import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/models/message.dart'; abstract class ApiMap { - ApiMap() { - var client = Dio()..interceptors.add(ConsoleInterceptor()); - (client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + Future getClient() async { + var dio = Dio(await options); + if (hasLoger) { + dio.interceptors.add(PrettyDioLogger()); + } + dio..interceptors.add(ConsoleInterceptor()); + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) { client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; return client; }; - loggedClient = client; + return dio; } - String? rootAddress; - late Dio loggedClient; + FutureOr get options; - void close() { - loggedClient.close(); + abstract final String rootAddress; + abstract final bool hasLoger; + abstract final bool isWithToken; + + ValidateStatus? validateStatus; + + void close(Dio client) { + client.close(); + validateStatus = null; } } diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index 5bf84863..2a9888cd 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -1,31 +1,36 @@ 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'; class BackblazeApi extends ApiMap { - BackblazeApi([String? token]) { - if (token != null) { - loggedClient.options = BaseOptions( - headers: {'Authorization': 'Basic $token'}, - baseUrl: rootAddress!, - ); + BackblazeApi({this.hasLoger = false, this.isWithToken = true}); + + BaseOptions get options { + var options = BaseOptions(baseUrl: rootAddress); + if (isWithToken) { + var backblazeCredential = getIt().backblazeCredential; + var token = backblazeCredential!.applicationKey; + options.headers = {'Authorization': 'Basic $token'}; } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + + return options; } @override - String? rootAddress = - 'https://api.backblazeb2.com/b2api/v2/b2_authorize_account'; + String rootAddress = 'https://api.backblazeb2.com/b2api/v2/'; - Future isValid(String token) async { - var options = Options( - headers: {'Authorization': 'Basic $token'}, - validateStatus: (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }, + Future isValid(String encodedApiKey) async { + var client = await getClient(); + Response response = await client.get( + 'b2_authorize_account', + options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), ); - - Response response = await loggedClient.get(rootAddress!, options: options); - + close(client); if (response.statusCode == HttpStatus.ok) { return true; } else if (response.statusCode == HttpStatus.unauthorized) { @@ -34,4 +39,10 @@ class BackblazeApi extends ApiMap { throw Exception('code: ${response.statusCode}'); } } -} \ No newline at end of file + + @override + bool hasLoger; + + @override + bool isWithToken; +} diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index b78b8ee6..7dc9994d 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -1,30 +1,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'; class CloudflareApi extends ApiMap { - CloudflareApi([String? token]) { - if (token != null) { - loggedClient.options = - BaseOptions(headers: {'Authorization': 'Bearer $token'}); + CloudflareApi({this.hasLoger = false, this.isWithToken = true}); + + BaseOptions get options { + var options = BaseOptions(baseUrl: rootAddress); + if (isWithToken) { + var token = getIt().cloudFlareKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + return options; } @override - String? rootAddress = 'https://api.cloudflare.com/client/v4'; + String rootAddress = 'https://api.cloudflare.com/client/v4'; Future isValid(String token) async { - var url = '$rootAddress/user/tokens/verify'; - var options = Options( - headers: {'Authorization': 'Bearer $token'}, - validateStatus: (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }, - ); + validateStatus = (status) { + return status == HttpStatus.ok || status == HttpStatus.unauthorized; + }; - Response response = await loggedClient.get(url, options: options); + var client = await getClient(); + Response response = await client.get('/user/tokens/verify', + options: Options(headers: {'Authorization': 'Bearer $token'})); + + close(client); if (response.statusCode == HttpStatus.ok) { return true; @@ -35,27 +45,19 @@ class CloudflareApi extends ApiMap { } } - Future getZoneId(String? token, String domain) async { - var url = '$rootAddress/zones'; - - var options = Options( - headers: {'Authorization': 'Bearer $token'}, - validateStatus: (status) { - return status == HttpStatus.ok || status == HttpStatus.forbidden; - }, - ); - - Response response = await loggedClient.get( - url, - options: options, + Future getZoneId(String domain) async { + validateStatus = (status) { + return status == HttpStatus.ok || status == HttpStatus.forbidden; + }; + var client = await getClient(); + Response response = await client.get( + '/zones', queryParameters: {'name': domain}, ); - try { - return response.data['result'][0]['id']; - } catch (error) { - return null; - } + close(client); + + return response.data['result'][0]['id']; } Future removeSimilarRecords({ @@ -65,20 +67,24 @@ class CloudflareApi extends ApiMap { var domainName = cloudFlareDomain.domainName; var domainZoneId = cloudFlareDomain.zoneId; - var url = '$rootAddress/zones/$domainZoneId/dns_records'; + var url = '/zones/$domainZoneId/dns_records'; + + var client = await getClient(); + Response response = await client.get(url); - var response = await loggedClient.get(url); List records = response.data['result'] ?? []; var allDeleteFutures = []; for (var record in records) { if (record['zone_name'] == domainName) { allDeleteFutures.add( - loggedClient.delete('$url/${record["id"]}'), + client.delete('$url/${record["id"]}'), ); } } + await Future.wait(allDeleteFutures); + close(client); } Future createMultipleDnsRecords({ @@ -92,10 +98,11 @@ class CloudflareApi extends ApiMap { var url = '$rootAddress/zones/$domainZoneId/dns_records'; var allCreateFutures = []; + var client = await getClient(); for (var record in listDnsRecords) { allCreateFutures.add( - loggedClient.post( + client.post( url, data: record.toJson(), ), @@ -103,23 +110,9 @@ class CloudflareApi extends ApiMap { } await Future.wait(allCreateFutures); + close(client); } - // setDkim(String dkimRecordString, String domainZoneId) { - // var txt3 = DnsRecords( - // type: 'TXT', - // name: 'selector._domainkey', - // content: dkimRecordString, - // ttl: 18000, - // ); - - // var url = '$rootAddress/zones/$domainZoneId/dns_records'; - // loggedClient.post( - // url, - // data: txt3.toJson(), - // ); - // } - List projectDnsRecords(String? domainName, String? ip4) { var domainA = DnsRecords(type: 'A', name: domainName, content: ip4); @@ -161,15 +154,24 @@ class CloudflareApi extends ApiMap { ]; } - Future?> domainList() async { + Future> domainList() async { var url = '$rootAddress/zones?per_page=50'; - var response = await loggedClient.get( + var client = await getClient(); + + var response = await client.get( url, queryParameters: {'per_page': 50}, ); + close(client); return response.data['result'] - .map((el) => el['name'] as String?) + .map((el) => el['name'] as String) .toList(); } + + @override + final bool hasLoger; + + @override + final bool isWithToken; } diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 8459d315..88efcb06 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -2,33 +2,49 @@ import 'dart:convert'; 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/utils/password_generator2.dart'; class HetznerApi extends ApiMap { - HetznerApi([String? token]) { - if (token != null) { - loggedClient.options = BaseOptions( - headers: {'Authorization': 'Bearer $token'}, - baseUrl: rootAddress!, - ); + bool hasLoger; + bool isWithToken; + + HetznerApi({this.hasLoger = false, this.isWithToken = true}); + + BaseOptions get options { + var options = BaseOptions(baseUrl: rootAddress); + if (isWithToken) { + var token = getIt().hetznerKey; + assert(token != null); + options.headers = {'Authorization': 'Bearer $token'}; } + + if (validateStatus != null) { + options.validateStatus = validateStatus!; + } + + return options; } @override - String? rootAddress = 'https://api.hetzner.cloud/v1/servers'; + String rootAddress = 'https://api.hetzner.cloud/v1'; Future isValid(String token) async { - var options = Options( - headers: {'Authorization': 'Bearer $token'}, - validateStatus: (status) { - return status == HttpStatus.ok || status == HttpStatus.unauthorized; - }, + validateStatus = (status) { + return status == HttpStatus.ok || status == HttpStatus.unauthorized; + }; + var client = await getClient(); + Response response = await client.get( + '/servers', + options: Options( + headers: {'Authorization': 'Bearer $token'}, + ), ); - - Response response = await loggedClient.get(rootAddress!, options: options); + close(client); if (response.statusCode == HttpStatus.ok) { return true; @@ -39,56 +55,128 @@ 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 createServer({ - required String? cloudFlareKey, + required String cloudFlareKey, required User rootUser, - required String? domainName, + required String domainName, }) async { var dbPassword = getRandomString(40); + const chars = + 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; + + var dbStorageName = getRandomString(6, chars); + var client = await getClient(); + + Response dbCreateResponse = await client.post( + '/volumes', + data: { + "size": 10, + "name": dbStorageName, + "labels": {"labelkey": "value"}, + "location": "fsn1", + "automount": false, + "format": "ext4" + }, + ); + var dbId = dbCreateResponse.data['volume']['id']; var data = jsonDecode( - '''{"name":"selfprivacy-server","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[],"networks":[],"user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-20.09 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} HASHED_PASSWORD=${rootUser.hashPassword} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":false}''', + '''{"name":"$domainName","server_type":"cx11","start_after_create":false,"image":"ubuntu-20.04", "volumes":[$dbId],"networks":[],"user_data":"#cloud-config\\nruncmd:\\n- curl https://git.selfprivacy.org/ilchub/selfprivacy-nixos-infect/raw/branch/master/nixos-infect | PROVIDER=hetzner NIX_CHANNEL=nixos-20.09 DOMAIN=$domainName LUSER=${rootUser.login} PASSWORD=${rootUser.password} HASHED_PASSWORD=${rootUser.hashPassword} CF_TOKEN=$cloudFlareKey DB_PASSWORD=$dbPassword bash 2>&1 | tee /tmp/infect.log","labels":{},"automount":true, "location": "fsn1"}''', ); - Response response = await loggedClient.post( - rootAddress!, + Response serverCreateResponse = await client.post( + '/servers', data: data, ); - + client.close(); return HetznerServerDetails( - id: response.data['server']['id'], - ip4: response.data['server']['public_net']['ipv4']['ip'], + id: serverCreateResponse.data['server']['id'], + ip4: serverCreateResponse.data['server']['public_net']['ipv4']['ip'], createTime: DateTime.now(), + dataBase: HetznerDataBase( + id: dbId, + name: dbCreateResponse.data['volume']['name'], + ), ); } - Future deleteSelfprivacyServer({ - required String? cloudFlareKey, + Future deleteSelfprivacyServerAndAllVolumes({ + required String domainName, }) async { - Response response = await loggedClient.get(rootAddress!); + var client = await getClient(); - List list = response.data['servers']; - var server = list.firstWhere((el) => el['name'] == 'selfprivacy-server'); - await loggedClient.delete('$rootAddress/${server['id']}'); + Response serversReponse = await client.get('/servers'); + List servers = serversReponse.data['servers']; + var server = servers.firstWhere((el) => el['name'] == domainName); + await client.delete('/servers/${server['id']}'); + + Response volumesReponse = await client.get('/volumes'); + List volumes = volumesReponse.data['volumes']; + + var laterFutures = []; + for (var volume in volumes) { + if (volume['server'] == null) { + await client.delete('/volumes/${volume['id']}'); + } else { + laterFutures.add(Future.delayed(Duration(seconds: 60)).then( + (_) => client.delete('/volumes/${volume['id']}'), + )); + } + } + + if (laterFutures.isEmpty) { + close(client); + } else { + Future.wait(laterFutures).then((value) => close(client)); + } } - Future startServer({ - required HetznerServerDetails server, - }) async { - await loggedClient.post('/${server.id}/actions/poweron'); + Future reset() async { + var server = getIt().hetznerServer!; - return server.copyWith( - startTime: DateTime.now(), - ); + var client = await getClient(); + await client.post('/servers/${server.id}/actions/reset'); + close(client); + + return server.copyWith(startTime: DateTime.now()); } - Future restart({ - required HetznerServerDetails server, - }) async { - await loggedClient.post('/${server.id}/actions/poweron'); + Future powerOn() async { + var server = getIt().hetznerServer!; - return server.copyWith( - startTime: DateTime.now(), - ); + var client = await getClient(); + await client.post('/servers/${server.id}/actions/poweron'); + close(client); + + return server.copyWith(startTime: DateTime.now()); + } + + metrics() async { + var hetznerServer = getIt().hetznerServer; + var client = await getClient(); + await client.post('/servers/${hetznerServer!.id}/metrics'); + close(client); + } + + Future getInfo() async { + var hetznerServer = getIt().hetznerServer; + var client = await getClient(); + Response response = await client.get('/servers/${hetznerServer!.id}'); + close(client); + + return HetznerServerInfo.fromJson(response.data!['server']); } } diff --git a/lib/logic/api_maps/server.dart b/lib/logic/api_maps/server.dart index 5f5abda4..2089e633 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -1,46 +1,45 @@ +import 'dart:async'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; import 'api_map.dart'; class ServerApi extends ApiMap { - ServerApi(String? domainName) { - loggedClient.options = BaseOptions( - baseUrl: 'https://api.$domainName', - ); + bool hasLoger; + bool isWithToken; + + ServerApi({this.hasLoger = false, this.isWithToken = true}); + + BaseOptions get options { + var options = BaseOptions(); + + if (isWithToken) { + var cloudFlareDomain = getIt().cloudFlareDomain; + var domainName = cloudFlareDomain!.domainName; + + options = BaseOptions(baseUrl: 'https://api.$domainName'); + } + + return options; } Future isHttpServerWorking() async { bool res; - Response response; + + var client = await getClient(); try { - response = await loggedClient.get('/serviceStatus'); + response = await client.get('/serviceStatus'); res = response.statusCode == HttpStatus.ok; } catch (e) { res = false; } - + close(client); return res; } - // Future getDkim(String domainName) async { - // var response = await loggedClient.get( - // '/getDKIM', - // options: Options(responseType: ResponseType.plain), - // ); - // return _decodeAndCutData(response.data, domainName); - // } + String get rootAddress => + throw UnimplementedError('not used in with implementation'); } - -// String _decodeAndCutData(String text, String domainName) { -// var decodedTextString = text.substring(1, text.length - 1); -// var stringToBase64 = utf8.fuse(base64); - -// return stringToBase64 -// .decode(decodedTextString) -// .replaceAll("selector._domainkey IN TXT ( ", "") -// .replaceAll("\"\n \"", "") -// .replaceAll(' ) ; ----- DKIM key selector for $domainName\n', ''); -// } diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart index 4d8dd8a5..6627821c 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -35,7 +35,10 @@ part 'app_config_state.dart'; /// c. if server is ok wait 30 sec /// d. reset server /// -/// 2.3. a. wait 60sec |finishCheckIfServerIsOkay +/// 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 @@ -44,21 +47,18 @@ class AppConfigCubit extends Cubit { final repository = AppConfigRepository(); - void load() { - var state = repository.load(); + Future load() async { + var state = await repository.load(); if (state.progress < 6 || state.isFullyInitilized) { emit(state); } else if (state.progress == 6) { - print('startServerIfDnsIsOkay'); - startServerIfDnsIsOkay(state: state, isImmediate: true); } else if (state.progress == 7) { - print('resetServerIfServerIsOkay'); - resetServerIfServerIsOkay(state: state, isImmediate: true); } else if (state.progress == 8) { - print('finishCheckIfServerIsOkay'); + oneMoreReset(state: state, isImmediate: true); + } else if (state.progress == 9) { finishCheckIfServerIsOkay(state: state, isImmediate: true); } } @@ -79,10 +79,11 @@ class AppConfigCubit extends Cubit { if (isMatch) { var server = await repository.startServer( - state.hetznerKey, state.hetznerServer!, ); - repository.saveServerDetails(server); + await repository.saveServerDetails(server); + await repository.saveIsServerStarted(true); + emit( state.copyWith( isServerStarted: true, @@ -110,41 +111,92 @@ class AppConfigCubit extends Cubit { } } - void resetServerIfServerIsOkay({ + void oneMoreReset({ AppConfigState? state, bool isImmediate = false, }) async { - state = state ?? this.state; + var dataState = state ?? this.state; var work = () async { - emit(TimerState(dataState: state!, isLoading: true)); + emit(TimerState(dataState: dataState, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking( - state.cloudFlareDomain!.domainName, - ); + var isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { var pauseDuration = Duration(seconds: 30); emit(TimerState( - dataState: state, + dataState: dataState, timerStart: DateTime.now(), isLoading: false, duration: pauseDuration, )); timer = Timer(pauseDuration, () async { - var hetznerServerDetails = await repository.restart( - state!.hetznerKey, - state.hetznerServer!, - ); + var hetznerServerDetails = await repository.restart(); + await repository.saveIsServerResetedSecondTime(true); + await repository.saveServerDetails(hetznerServerDetails); + emit( - state.copyWith( - isServerReseted: true, + 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({ + AppConfigState? state, + bool isImmediate = false, + }) async { + var dataState = state ?? this.state; + + 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(); } @@ -155,7 +207,7 @@ class AppConfigCubit extends Cubit { var pauseDuration = Duration(seconds: 60); emit( TimerState( - dataState: state, + dataState: dataState, timerStart: DateTime.now(), duration: pauseDuration, isLoading: false, @@ -176,12 +228,15 @@ class AppConfigCubit extends Cubit { var work = () async { emit(TimerState(dataState: state!, isLoading: true)); - var isServerWorking = await repository.isHttpServerWorking( - state.cloudFlareDomain!.domainName, - ); + var isServerWorking = await repository.isHttpServerWorking(); if (isServerWorking) { - emit(state.copyWith(hasFinalChecked: true, isLoading: false)); + await repository.saveHasFinalChecked(true); + + emit(state.copyWith( + hasFinalChecked: true, + isLoading: false, + )); } else { finishCheckIfServerIsOkay(); } @@ -203,37 +258,37 @@ class AppConfigCubit extends Cubit { } void clearAppConfig() { - _closeTimer(); + closeTimer(); repository.clearAppConfig(); emit(InitialAppConfigState()); } - void setHetznerKey(String hetznerKey) { - repository.saveHetznerKey(hetznerKey); + void setHetznerKey(String hetznerKey) async { + await repository.saveHetznerKey(hetznerKey); emit(state.copyWith(hetznerKey: hetznerKey)); } - void setCloudflareKey(String cloudFlareKey) { - repository.saveCloudFlare(cloudFlareKey); + void setCloudflareKey(String cloudFlareKey) async { + await repository.saveCloudFlareKey(cloudFlareKey); emit(state.copyWith(cloudFlareKey: cloudFlareKey)); } - void setBackblazeKey(String keyId, String applicationKey) { + void setBackblazeKey(String keyId, String applicationKey) async { var backblazeCredential = BackblazeCredential( keyId: keyId, applicationKey: applicationKey, ); - repository.saveBackblazeKey(backblazeCredential); + await repository.saveBackblazeKey(backblazeCredential); emit(state.copyWith(backblazeCredential: backblazeCredential)); } - void setDomain(CloudFlareDomain cloudFlareDomain) { - repository.saveDomain(cloudFlareDomain); + void setDomain(CloudFlareDomain cloudFlareDomain) async { + await repository.saveDomain(cloudFlareDomain); emit(state.copyWith(cloudFlareDomain: cloudFlareDomain)); } - void setRootUser(User rootUser) { - repository.saveRootUser(rootUser); + void setRootUser(User rootUser) async { + await repository.saveRootUser(rootUser); emit(state.copyWith(rootUser: rootUser)); } @@ -241,7 +296,6 @@ class AppConfigCubit extends Cubit { AppConfigState _stateCopy = state; var onSuccess = (serverDetails) async { await repository.createDnsRecords( - state.cloudFlareKey, serverDetails.ip4, state.cloudFlareDomain!, ); @@ -258,25 +312,23 @@ class AppConfigCubit extends Cubit { try { emit(state.copyWith(isLoading: true)); await repository.createServer( - state.hetznerKey, state.rootUser!, state.cloudFlareDomain!.domainName, - state.cloudFlareKey, + state.cloudFlareKey!, onCancel: onCancel, onSuccess: onSuccess, ); } catch (e) { - addError(e); emit(_stateCopy); } } close() { - _closeTimer(); + closeTimer(); return super.close(); } - void _closeTimer() { + 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 index 1a9f9d2b..ebbbb305 100644 --- a/lib/logic/cubit/app_config/app_config_repository.dart +++ b/lib/logic/cubit/app_config/app_config_repository.dart @@ -4,6 +4,7 @@ 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/get_it/api_config.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'; @@ -15,68 +16,46 @@ import 'package:basic_utils/basic_utils.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'; +import 'package:easy_localization/easy_localization.dart'; class AppConfigRepository { Box box = Hive.box(BNames.appConfig); - AppConfigState load() { - return AppConfigState( - hetznerKey: box.get(BNames.hetznerKey), - cloudFlareKey: box.get(BNames.cloudFlareKey), - cloudFlareDomain: box.get(BNames.cloudFlareDomain), - backblazeCredential: box.get(BNames.backblazeKey), + Future load() async { + var res = AppConfigState( + hetznerKey: getIt().hetznerKey, + cloudFlareKey: getIt().cloudFlareKey, + cloudFlareDomain: getIt().cloudFlareDomain, + backblazeCredential: getIt().backblazeCredential, + hetznerServer: getIt().hetznerServer, rootUser: box.get(BNames.rootUser), - hetznerServer: box.get(BNames.hetznerServer), isServerStarted: box.get(BNames.isServerStarted, defaultValue: false), - error: null, + isServerResetedFirstTime: + box.get(BNames.isServerResetedFirstTime, defaultValue: false), + isServerResetedSecondTime: + box.get(BNames.isServerResetedSecondTime, defaultValue: false), hasFinalChecked: box.get(BNames.hasFinalChecked, defaultValue: false), + error: null, isLoading: box.get(BNames.isLoading, defaultValue: false), - isServerReseted: box.get(BNames.isServerReseted, defaultValue: false), ); + + return res; } void clearAppConfig() { box.clear(); } - void saveHetznerKey(String key) { - box.put(BNames.hetznerKey, key); - } - - void saveBackblazeKey(BackblazeCredential backblazeCredential) { - box.put(BNames.backblazeKey, backblazeCredential); - } - - void saveCloudFlare(String key) { - box.put(BNames.cloudFlareKey, key); - } - - void saveDomain(CloudFlareDomain cloudFlareDomain) { - box.put(BNames.cloudFlareDomain, cloudFlareDomain); - } - - void saveRootUser(User rootUser) { - box.put(BNames.rootUser, rootUser); - } - Future startServer( - String? hetznerKey, HetznerServerDetails hetznerServer, ) async { - var hetznerApi = HetznerApi(hetznerKey); - var serverDetails = await hetznerApi.startServer(server: hetznerServer); - hetznerApi.close(); - box.put(BNames.isServerStarted, true); + var hetznerApi = HetznerApi(); + var serverDetails = await hetznerApi.powerOn(); return serverDetails; } - Future saveServerDetails(HetznerServerDetails serverDetails) async { - await box.put(BNames.hetznerServer, serverDetails); - } - Future isDnsAddressesMatch(String? domainName, String? ip4) async { - print(domainName); var addresses = [ '$domainName', 'api.$domainName', @@ -110,20 +89,18 @@ class AppConfigRepository { } } - box.put(BNames.hasFinalChecked, true); - return true; } Future createServer( - String? hetznerKey, User rootUser, - String? domainName, - String? cloudFlareKey, { - void Function()? onCancel, - required Future Function(HetznerServerDetails serverDetails) onSuccess, + String domainName, + String cloudFlareKey, { + required void Function() onCancel, + required Future Function(HetznerServerDetails serverDetails) + onSuccess, }) async { - var hetznerApi = HetznerApi(hetznerKey); + var hetznerApi = HetznerApi(); try { var serverDetails = await hetznerApi.createServer( @@ -131,41 +108,37 @@ class AppConfigRepository { rootUser: rootUser, domainName: domainName, ); - await box.put(BNames.hetznerServer, serverDetails); - hetznerApi.close(); + saveServerDetails(serverDetails); onSuccess(serverDetails); } on DioError catch (e) { if (e.response!.data['error']['code'] == 'uniqueness_error') { var nav = getIt.get(); nav.showPopUpDialog( BrandAlert( - title: 'Сервер с таким именем уже существует', - contentText: 'Уничтожить сервер и создать новый?', + title: 'modals.1'.tr(), + contentText: 'modals.2'.tr(), acitons: [ ActionButton( - text: 'Удалить', + text: 'basis.delete'.tr(), isRed: true, onPressed: () async { - await hetznerApi.deleteSelfprivacyServer( - cloudFlareKey: cloudFlareKey, - ); + await hetznerApi.deleteSelfprivacyServerAndAllVolumes( + domainName: domainName); var serverDetails = await hetznerApi.createServer( cloudFlareKey: cloudFlareKey, rootUser: rootUser, domainName: domainName, ); - hetznerApi.close(); - await box.put(BNames.hetznerServer, serverDetails); + await saveServerDetails(serverDetails); onSuccess(serverDetails); }, ), ActionButton( - text: 'Отменить', + text: 'basis.cancel'.tr(), onPressed: () { - hetznerApi.close(); - onCancel!(); + onCancel(); }, ), ], @@ -176,11 +149,10 @@ class AppConfigRepository { } Future createDnsRecords( - String? cloudFlareKey, String? ip4, CloudFlareDomain cloudFlareDomain, ) async { - var cloudflareApi = CloudflareApi(cloudFlareKey); + var cloudflareApi = CloudflareApi(); await cloudflareApi.removeSimilarRecords( ip4: ip4, @@ -191,22 +163,61 @@ class AppConfigRepository { ip4: ip4, cloudFlareDomain: cloudFlareDomain, ); - - cloudflareApi.close(); } - Future isHttpServerWorking(String? domainName) async { - var api = ServerApi(domainName); + Future isHttpServerWorking() async { + var api = ServerApi(); var isHttpServerWorking = await api.isHttpServerWorking(); - api.close(); return isHttpServerWorking; } - Future restart( - String? hetznerKey, - HetznerServerDetails server, - ) async { - var hetznerApi = HetznerApi(hetznerKey); - return await hetznerApi.restart(server: server); + 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 { + 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); } } diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart index d49c3382..50040e81 100644 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ b/lib/logic/cubit/app_config/app_config_state.dart @@ -9,7 +9,8 @@ class AppConfigState extends Equatable { required this.rootUser, required this.hetznerServer, required this.isServerStarted, - required this.isServerReseted, + required this.isServerResetedFirstTime, + required this.isServerResetedSecondTime, required this.hasFinalChecked, required this.isLoading, required this.error, @@ -24,7 +25,7 @@ class AppConfigState extends Equatable { rootUser, hetznerServer, isServerStarted, - isServerReseted, + isServerResetedFirstTime, hasFinalChecked, isLoading, error, @@ -36,9 +37,11 @@ class AppConfigState extends Equatable { final CloudFlareDomain? cloudFlareDomain; final User? rootUser; final HetznerServerDetails? hetznerServer; - final bool? isServerStarted; - final bool? isServerReseted; - final bool? hasFinalChecked; + final bool isServerStarted; + final bool isServerResetedFirstTime; + final bool isServerResetedSecondTime; + + final bool hasFinalChecked; final bool? isLoading; final Exception? error; @@ -51,7 +54,8 @@ class AppConfigState extends Equatable { User? rootUser, HetznerServerDetails? hetznerServer, bool? isServerStarted, - bool? isServerReseted, + bool? isServerResetedFirstTime, + bool? isServerResetedSecondTime, bool? hasFinalChecked, bool? isLoading, Exception? error, @@ -64,7 +68,10 @@ class AppConfigState extends Equatable { rootUser: rootUser ?? this.rootUser, hetznerServer: hetznerServer ?? this.hetznerServer, isServerStarted: isServerStarted ?? this.isServerStarted, - isServerReseted: isServerReseted ?? this.isServerReseted, + isServerResetedFirstTime: + isServerResetedFirstTime ?? this.isServerResetedFirstTime, + isServerResetedSecondTime: + isServerResetedSecondTime ?? this.isServerResetedSecondTime, hasFinalChecked: hasFinalChecked ?? this.hasFinalChecked, isLoading: isLoading ?? this.isLoading, error: error ?? this.error, @@ -80,17 +87,22 @@ class AppConfigState extends Equatable { bool get isFullyInitilized => _fulfilementList.every((el) => el!); int get progress => _fulfilementList.where((el) => el!).length; - List get _fulfilementList => [ - isHetznerFilled, - isCloudFlareFilled, - isBackblazeFilled, - isDomainFilled, - isUserFilled, - isServerCreated, - isServerStarted, - isServerReseted, - hasFinalChecked, - ]; + List get _fulfilementList { + var res = [ + isHetznerFilled, + isCloudFlareFilled, + isBackblazeFilled, + isDomainFilled, + isUserFilled, + isServerCreated, + isServerStarted, + isServerResetedFirstTime, + isServerResetedSecondTime, + hasFinalChecked, + ]; + + return res; + } } class InitialAppConfigState extends AppConfigState { @@ -103,7 +115,8 @@ class InitialAppConfigState extends AppConfigState { rootUser: null, hetznerServer: null, isServerStarted: false, - isServerReseted: false, + isServerResetedFirstTime: false, + isServerResetedSecondTime: false, hasFinalChecked: false, isLoading: false, error: null, @@ -124,7 +137,8 @@ class TimerState extends AppConfigState { rootUser: dataState.rootUser, hetznerServer: dataState.hetznerServer, isServerStarted: dataState.isServerStarted, - isServerReseted: dataState.isServerReseted, + isServerResetedFirstTime: dataState.isServerResetedFirstTime, + isServerResetedSecondTime: dataState.isServerResetedSecondTime, hasFinalChecked: dataState.hasFinalChecked, isLoading: isLoading, error: dataState.error, diff --git a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart b/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart index 386882a9..123553cf 100644 --- a/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/backblaze_form_cubit.dart @@ -5,8 +5,6 @@ import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/models/backblaze_credential.dart'; class BackblazeFormCubit extends FormCubit { - BackblazeApi apiClient = BackblazeApi(); - BackblazeFormCubit(this.initializingCubit) { //var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); keyId = FieldCubit( @@ -29,7 +27,7 @@ class BackblazeFormCubit extends FormCubit { ], ); - super.setFields([keyId, applicationKey]); + super.addFields([keyId, applicationKey]); } @override @@ -42,14 +40,14 @@ class BackblazeFormCubit extends FormCubit { final AppConfigCubit initializingCubit; - // ignore: close_sinks late final FieldCubit keyId; - // ignore: close_sinks late final FieldCubit applicationKey; @override FutureOr asyncValidation() async { late bool isKeyValid; + BackblazeApi apiClient = BackblazeApi(isWithToken: false); + try { String encodedApiKey = encodedBackblazeKey( keyId.state.value, @@ -67,11 +65,4 @@ class BackblazeFormCubit extends FormCubit { } return true; } - - @override - Future close() async { - apiClient.close(); - - return super.close(); - } } diff --git a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart index 2f885436..883470c8 100644 --- a/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/cloudflare_form_cubit.dart @@ -6,8 +6,6 @@ import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; class CloudFlareFormCubit extends FormCubit { - CloudflareApi apiClient = CloudflareApi(); - CloudFlareFormCubit(this.initializingCubit) { var regExp = RegExp(r"\s+|[!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); apiKey = FieldCubit( @@ -20,7 +18,7 @@ class CloudFlareFormCubit extends FormCubit { ], ); - super.setFields([apiKey]); + super.addFields([apiKey]); } @override @@ -35,6 +33,7 @@ class CloudFlareFormCubit extends FormCubit { @override FutureOr asyncValidation() async { late bool isKeyValid; + CloudflareApi apiClient = CloudflareApi(isWithToken: false); try { isKeyValid = await apiClient.isValid(apiKey.state.value); @@ -51,8 +50,6 @@ class CloudFlareFormCubit extends FormCubit { @override Future close() async { - apiClient.close(); - return super.close(); } } diff --git a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart b/lib/logic/cubit/forms/initializing/domain_cloudflare.dart index 07616b0a..1551027c 100644 --- a/lib/logic/cubit/forms/initializing/domain_cloudflare.dart +++ b/lib/logic/cubit/forms/initializing/domain_cloudflare.dart @@ -4,20 +4,15 @@ import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; class DomainSetupCubit extends Cubit { - DomainSetupCubit(this.initializingCubit) : super(Initial()) { - var token = initializingCubit.state.cloudFlareKey; + DomainSetupCubit(this.initializingCubit) : super(Initial()); - assert(token != null, 'no cloudflare token'); - - api = CloudflareApi(token); - } - - AppConfigCubit initializingCubit; - late CloudflareApi api; + final AppConfigCubit initializingCubit; Future load() async { emit(Loading(LoadingTypes.loadingDomain)); - var list = await (api.domainList() as Future>); + var api = CloudflareApi(); + + var list = await api.domainList(); if (list.isEmpty) { emit(Empty()); } else if (list.length == 1) { @@ -29,20 +24,17 @@ class DomainSetupCubit extends Cubit { @override Future close() { - api.close(); return super.close(); } Future saveDomain() async { assert(state is Loaded, 'wrong state'); var domainName = (state as Loaded).domain; + var api = CloudflareApi(); emit(Loading(LoadingTypes.saving)); - var zoneId = await api.getZoneId( - initializingCubit.state.cloudFlareKey, - domainName, - ); + var zoneId = await api.getZoneId(domainName); var domain = CloudFlareDomain( domainName: domainName, diff --git a/lib/logic/cubit/forms/initializing/domain_form_cubit.dart b/lib/logic/cubit/forms/initializing/domain_form_cubit.dart deleted file mode 100644 index e2dfad83..00000000 --- a/lib/logic/cubit/forms/initializing/domain_form_cubit.dart +++ /dev/null @@ -1,68 +0,0 @@ -// import 'dart:async'; - -// 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'; - -// class DomainFormCubit extends FormCubit { -// CloudflareApi apiClient = CloudflareApi(); - -// DomainFormCubit(this.initializingCubit) { -// var regExp = -// RegExp(r"^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}"); -// domainName = FieldCubit( -// initalValue: '', -// validations: [ -// RequiredStringValidation('required'), -// ValidationModel( -// (s) => !regExp.hasMatch(s), -// 'invalid domain format', -// ), -// ], -// ); - -// super.setFields([domainName]); -// } - -// @override -// FutureOr onSubmit() async { -// var domain = CloudFlareDomain( -// domainName: domainName.state.value, -// zoneId: zoneId, -// ); -// initializingCubit.setDomain(domain); -// } - -// final AppConfigCubit initializingCubit; - -// FieldCubit domainName; -// String zoneId; - -// @override -// FutureOr asyncValidation() async { -// var key = initializingCubit.state.cloudFlareKey; - -// String zoneId; - -// try { -// zoneId = await apiClient.getZoneId(key, domainName.state.value); -// } catch (e) { -// addError(e); -// } - -// if (zoneId == null) { -// domainName.setError('Domain not in the list'); -// return false; -// } -// this.zoneId = zoneId; -// return true; -// } - -// @override -// Future close() async { -// apiClient.close(); - -// return super.close(); -// } -// } diff --git a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart index 2383a986..a9e89501 100644 --- a/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/hetzner_form_cubit.dart @@ -6,8 +6,6 @@ import 'package:selfprivacy/logic/cubit/forms/validations/validations.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; class HetznerFormCubit extends FormCubit { - HetznerApi apiClient = HetznerApi(); - HetznerFormCubit(this.initializingCubit) { var regExp = RegExp(r"\s+|[-!$%^&*()@+|~=`{}\[\]:<>?,.\/]"); apiKey = FieldCubit( @@ -20,7 +18,7 @@ class HetznerFormCubit extends FormCubit { ], ); - super.setFields([apiKey]); + super.addFields([apiKey]); } @override @@ -30,12 +28,13 @@ class HetznerFormCubit extends FormCubit { final AppConfigCubit initializingCubit; - // ignore: close_sinks late final FieldCubit apiKey; @override FutureOr asyncValidation() async { late bool isKeyValid; + HetznerApi apiClient = HetznerApi(isWithToken: false); + try { isKeyValid = await apiClient.isValid(apiKey.state.value); } catch (e) { @@ -48,11 +47,4 @@ class HetznerFormCubit extends FormCubit { } return true; } - - @override - Future close() async { - apiClient.close(); - - return super.close(); - } } diff --git a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart b/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart index f651c251..b82bd0f3 100644 --- a/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart +++ b/lib/logic/cubit/forms/initializing/root_user_form_cubit.dart @@ -1,13 +1,10 @@ import 'dart:async'; import 'package:cubit_form/cubit_form.dart'; -import 'package:selfprivacy/logic/api_maps/hetzner.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/models/user.dart'; class RootUserFormCubit extends FormCubit { - HetznerApi apiClient = HetznerApi(); - RootUserFormCubit(this.initializingCubit) { var userRegExp = RegExp(r"\W"); var passwordRegExp = RegExp(r"[\n\r\s]+"); @@ -32,7 +29,7 @@ class RootUserFormCubit extends FormCubit { isVisible = FieldCubit(initalValue: false); - super.setFields([userName, password, isVisible]); + super.addFields([userName, password, isVisible]); } @override @@ -46,17 +43,7 @@ class RootUserFormCubit extends FormCubit { final AppConfigCubit initializingCubit; - // ignore: close_sinks late final FieldCubit userName; - // ignore: close_sinks late final FieldCubit password; - // ignore: close_sinks late final FieldCubit isVisible; - - @override - Future close() async { - apiClient.close(); - - return super.close(); - } } diff --git a/lib/logic/cubit/forms/user/user_form_cubit.dart b/lib/logic/cubit/forms/user/user_form_cubit.dart index 59bd50d3..50de369e 100644 --- a/lib/logic/cubit/forms/user/user_form_cubit.dart +++ b/lib/logic/cubit/forms/user/user_form_cubit.dart @@ -33,7 +33,7 @@ class UserFormCubit extends FormCubit { ], ); - super.setFields([login, password]); + super.addFields([login, password]); } @override @@ -45,7 +45,6 @@ class UserFormCubit extends FormCubit { usersCubit.addUser(user); } - // ignore: close_sinks late FieldCubit login; late FieldCubit password; 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 new file mode 100644 index 00000000..3d46142d --- /dev/null +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart @@ -0,0 +1,24 @@ +import 'package:bloc/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/hetzner_server_info.dart'; + +part 'server_detailed_info_state.dart'; + +class ServerDetailsCubit extends Cubit { + ServerDetailsCubit() : super(ServerDetailsInitial()); + + ServerDetailsRepository repository = ServerDetailsRepository(); + + void check() async { + var isReadyToCheck = getIt().hetznerServer != null; + if (isReadyToCheck) { + emit(ServerDetailsLoading()); + var data = await repository.load(); + emit(Loaded(serverInfo: data, 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 new file mode 100644 index 00000000..bcd34625 --- /dev/null +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart @@ -0,0 +1,9 @@ +import 'package:selfprivacy/logic/api_maps/hetzner.dart'; +import 'package:selfprivacy/logic/models/hetzner_server_info.dart'; + +class ServerDetailsRepository { + Future load() async { + var client = HetznerApi(); + return await client.getInfo(); + } +} 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 new file mode 100644 index 00000000..cf017658 --- /dev/null +++ b/lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart @@ -0,0 +1,29 @@ +part of 'server_detailed_info_cubit.dart'; + +abstract class ServerDetailsState extends Equatable { + const ServerDetailsState(); + + @override + List get props => []; +} + +class ServerDetailsInitial extends ServerDetailsState {} + +class ServerDetailsLoading extends ServerDetailsState {} + +class ServerDetailsNotReady extends ServerDetailsState {} + +class Loading extends ServerDetailsState {} + +class Loaded extends ServerDetailsState { + final HetznerServerInfo serverInfo; + final DateTime checkTime; + + Loaded({ + required this.serverInfo, + required this.checkTime, + }); + + @override + List get props => [serverInfo, checkTime]; +} diff --git a/lib/logic/cubit/services/services_cubit.dart b/lib/logic/cubit/services/services_cubit.dart deleted file mode 100644 index a23b70e7..00000000 --- a/lib/logic/cubit/services/services_cubit.dart +++ /dev/null @@ -1,28 +0,0 @@ -// import 'package:bloc/bloc.dart'; -// import 'package:equatable/equatable.dart'; -// import 'package:meta/meta.dart'; -// import 'package:selfprivacy/logic/models/service.dart'; -// import 'package:selfprivacy/logic/models/state_types.dart'; - -// export 'package:provider/provider.dart'; -// export 'package:selfprivacy/logic/models/state_types.dart'; - -// part 'services_state.dart'; - -// class ServicesCubit extends Cubit { -// ServicesCubit() : super(ServicesState(all)); - -// void connect(Service service) { -// var newState = state.updateElement(service, StateType.stable); -// emit(newState); -// } -// } - -// final all = ServiceTypes.values -// .map( -// (type) => Service( -// state: StateType.uninitialized, -// type: type, -// ), -// ) -// .toList(); diff --git a/lib/logic/cubit/services/services_state.dart b/lib/logic/cubit/services/services_state.dart deleted file mode 100644 index d20a63a8..00000000 --- a/lib/logic/cubit/services/services_state.dart +++ /dev/null @@ -1,26 +0,0 @@ -// part of 'services_cubit.dart'; - -// @immutable -// class ServicesState extends Equatable{ -// ServicesState(this.all); - -// final List all; - -// ServicesState updateElement(Service service, StateType newState) { -// var newList = [...all]; -// var index = newList.indexOf(service); -// newList[index] = service.updateState(newState); -// return ServicesState(newList); -// } - -// List get connected => all -// .where((service) => service.state != StateType.uninitialized) -// .toList(); - -// List get uninitialized => all -// .where((service) => service.state == StateType.uninitialized) -// .toList(); - -// @override -// List get props => all; -// } diff --git a/lib/logic/get_it/api_config.dart b/lib/logic/get_it/api_config.dart new file mode 100644 index 00000000..6c5636d1 --- /dev/null +++ b/lib/logic/get_it/api_config.dart @@ -0,0 +1,63 @@ +import 'package:hive/hive.dart'; +import 'package:selfprivacy/config/hive_config.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'; + +class ApiConfigModel { + Box _box = Hive.box(BNames.appConfig); + + HetznerServerDetails? get hetznerServer => _hetznerServer; + String? get hetznerKey => _hetznerKey; + String? get cloudFlareKey => _cloudFlareKey; + BackblazeCredential? get backblazeCredential => _backblazeCredential; + CloudFlareDomain? get cloudFlareDomain => _cloudFlareDomain; + + String? _hetznerKey; + String? _cloudFlareKey; + HetznerServerDetails? _hetznerServer; + BackblazeCredential? _backblazeCredential; + CloudFlareDomain? _cloudFlareDomain; + + Future storeHetznerKey(String value) async { + await _box.put(BNames.hetznerKey, value); + _hetznerKey = value; + } + + Future storeCloudFlareKey(String value) async { + await _box.put(BNames.cloudFlareKey, value); + _cloudFlareKey = value; + } + + Future storeBackblazeCredential(BackblazeCredential value) async { + await _box.put(BNames.backblazeKey, value); + + _backblazeCredential = value; + } + + Future storeCloudFlareDomain(CloudFlareDomain value) async { + await _box.put(BNames.cloudFlareDomain, value); + _cloudFlareDomain = value; + } + + Future storeServerDetails(HetznerServerDetails value) async { + await _box.put(BNames.hetznerServer, value); + _hetznerServer = value; + } + + clear() { + _hetznerKey = null; + _cloudFlareKey = null; + _backblazeCredential = null; + _cloudFlareDomain = null; + _hetznerServer = null; + } + + void init() { + _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); + } +} diff --git a/lib/logic/get_it/timer.dart b/lib/logic/get_it/timer.dart index 0444b44e..e91d3a20 100644 --- a/lib/logic/get_it/timer.dart +++ b/lib/logic/get_it/timer.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; class TimerModel extends ChangeNotifier { DateTime _time = DateTime.now(); - DateTime get messages => _time; + DateTime get time => _time; void restart() { _time = DateTime.now(); diff --git a/lib/logic/models/backblaze_credential.dart b/lib/logic/models/backblaze_credential.dart index 3496f4ce..b9c06364 100644 --- a/lib/logic/models/backblaze_credential.dart +++ b/lib/logic/models/backblaze_credential.dart @@ -6,13 +6,13 @@ part 'backblaze_credential.g.dart'; @HiveType(typeId: 4) class BackblazeCredential { - BackblazeCredential({this.keyId, this.applicationKey}); + BackblazeCredential({required this.keyId, required this.applicationKey}); @HiveField(0) - final String? keyId; + final String keyId; @HiveField(1) - final String? applicationKey; + final String applicationKey; get encodedApiKey => encodedBackblazeKey(keyId, applicationKey); diff --git a/lib/logic/models/backblaze_credential.g.dart b/lib/logic/models/backblaze_credential.g.dart index 305b3386..c6ad373e 100644 --- a/lib/logic/models/backblaze_credential.g.dart +++ b/lib/logic/models/backblaze_credential.g.dart @@ -17,8 +17,8 @@ class BackblazeCredentialAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return BackblazeCredential( - keyId: fields[0] as String?, - applicationKey: fields[1] as String?, + keyId: fields[0] as String, + applicationKey: fields[1] as String, ); } diff --git a/lib/logic/models/cloudflare_domain.dart b/lib/logic/models/cloudflare_domain.dart index d66e9a04..9d85bfb1 100644 --- a/lib/logic/models/cloudflare_domain.dart +++ b/lib/logic/models/cloudflare_domain.dart @@ -4,13 +4,16 @@ part 'cloudflare_domain.g.dart'; @HiveType(typeId: 3) class CloudFlareDomain { - CloudFlareDomain({this.domainName, this.zoneId}); + CloudFlareDomain({ + required this.domainName, + required this.zoneId, + }); @HiveField(0) - final String? domainName; + final String domainName; @HiveField(1) - final String? zoneId; + final String zoneId; @override String toString() { diff --git a/lib/logic/models/cloudflare_domain.g.dart b/lib/logic/models/cloudflare_domain.g.dart index d96cf9bc..dcd95317 100644 --- a/lib/logic/models/cloudflare_domain.g.dart +++ b/lib/logic/models/cloudflare_domain.g.dart @@ -17,8 +17,8 @@ class CloudFlareDomainAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return CloudFlareDomain( - domainName: fields[0] as String?, - zoneId: fields[1] as String?, + domainName: fields[0] as String, + zoneId: fields[1] as String, ); } diff --git a/lib/logic/models/hetzner_server_info.dart b/lib/logic/models/hetzner_server_info.dart new file mode 100644 index 00000000..98af1c3e --- /dev/null +++ b/lib/logic/models/hetzner_server_info.dart @@ -0,0 +1,89 @@ +import 'package:json_annotation/json_annotation.dart'; + +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, + this.status, + this.created, + this.serverType, + this.location, + ); +} + +enum ServerStatus { + running, + initializing, + starting, + stopping, + off, + deleting, + migrating, + rebuilding, + unknown, +} + +@JsonSerializable() +class HetznerServerTypeInfo { + 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) => + _$HetznerServerTypeInfoFromJson(json); +} + +@JsonSerializable() +class HetznerPriceInfo { + HetznerPriceInfo(this.hourly, this.monthly); + + @JsonKey(name: 'price_hourly', fromJson: HetznerPriceInfo.getPrice) + final double hourly; + + @JsonKey(name: 'price_monthly', fromJson: HetznerPriceInfo.getPrice) + final double monthly; + + static HetznerPriceInfo fromJson(Map json) => + _$HetznerPriceInfoFromJson(json); + + static double getPrice(Map json) => double.parse(json['gross'] as String); +} + +@JsonSerializable() +class HetznerLocation { + final String country; + final String city; + final String description; + + @JsonKey(name: 'network_zone') + final String zone; + + HetznerLocation(this.country, this.city, this.description, this.zone); + + static HetznerLocation fromJson(Map json) => + _$HetznerLocationFromJson(json); +} diff --git a/lib/logic/models/hetzner_server_info.g.dart b/lib/logic/models/hetzner_server_info.g.dart new file mode 100644 index 00000000..40055d19 --- /dev/null +++ b/lib/logic/models/hetzner_server_info.g.dart @@ -0,0 +1,84 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'hetzner_server_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +HetznerServerInfo _$HetznerServerInfoFromJson(Map json) { + return HetznerServerInfo( + json['id'] as int, + json['name'] as String, + _$enumDecode(_$ServerStatusEnumMap, json['status']), + DateTime.parse(json['created'] as String), + HetznerServerTypeInfo.fromJson(json['server_type'] as Map), + HetznerServerInfo.locationFromJson(json['datacenter'] as Map), + ); +} + +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, +}) { + if (source == null) { + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); + } + + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; +} + +const _$ServerStatusEnumMap = { + ServerStatus.running: 'running', + ServerStatus.initializing: 'initializing', + ServerStatus.starting: 'starting', + ServerStatus.stopping: 'stopping', + ServerStatus.off: 'off', + ServerStatus.deleting: 'deleting', + ServerStatus.migrating: 'migrating', + ServerStatus.rebuilding: 'rebuilding', + ServerStatus.unknown: 'unknown', +}; + +HetznerServerTypeInfo _$HetznerServerTypeInfoFromJson( + Map json) { + return HetznerServerTypeInfo( + json['cores'] as int, + json['memory'] as num, + json['disk'] as int, + (json['prices'] as List) + .map((e) => HetznerPriceInfo.fromJson(e as Map)) + .toList(), + ); +} + +HetznerPriceInfo _$HetznerPriceInfoFromJson(Map json) { + return HetznerPriceInfo( + HetznerPriceInfo.getPrice(json['price_hourly'] as Map), + HetznerPriceInfo.getPrice(json['price_monthly'] as Map), + ); +} + +HetznerLocation _$HetznerLocationFromJson(Map json) { + return HetznerLocation( + json['country'] as String, + json['city'] as String, + json['description'] as String, + json['network_zone'] as String, + ); +} diff --git a/lib/logic/models/server_details.dart b/lib/logic/models/server_details.dart index e014de14..4afbc5cd 100644 --- a/lib/logic/models/server_details.dart +++ b/lib/logic/models/server_details.dart @@ -8,14 +8,15 @@ class HetznerServerDetails { required this.ip4, required this.id, required this.createTime, + required this.dataBase, this.startTime, }); @HiveField(0) - final String? ip4; + final String ip4; @HiveField(1) - final int? id; + final int id; @HiveField(3) final DateTime? createTime; @@ -23,14 +24,31 @@ class HetznerServerDetails { @HiveField(2) final DateTime? startTime; + @HiveField(4) + final HetznerDataBase dataBase; + HetznerServerDetails copyWith({DateTime? startTime}) { return HetznerServerDetails( startTime: startTime ?? this.startTime, createTime: createTime, id: id, ip4: ip4, + dataBase: dataBase, ); } 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_details.g.dart b/lib/logic/models/server_details.g.dart index 0f08e2f2..d59c9020 100644 --- a/lib/logic/models/server_details.g.dart +++ b/lib/logic/models/server_details.g.dart @@ -17,9 +17,10 @@ class HetznerServerDetailsAdapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return HetznerServerDetails( - ip4: fields[0] as String?, - id: fields[1] as int?, + ip4: fields[0] as String, + id: fields[1] as int, createTime: fields[3] as DateTime?, + dataBase: fields[4] as HetznerDataBase, startTime: fields[2] as DateTime?, ); } @@ -27,7 +28,7 @@ class HetznerServerDetailsAdapter extends TypeAdapter { @override void write(BinaryWriter writer, HetznerServerDetails obj) { writer - ..writeByte(4) + ..writeByte(5) ..writeByte(0) ..write(obj.ip4) ..writeByte(1) @@ -35,7 +36,9 @@ class HetznerServerDetailsAdapter extends TypeAdapter { ..writeByte(3) ..write(obj.createTime) ..writeByte(2) - ..write(obj.startTime); + ..write(obj.startTime) + ..writeByte(4) + ..write(obj.dataBase); } @override @@ -48,3 +51,40 @@ class HetznerServerDetailsAdapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class HetznerDataBaseAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + HetznerDataBase read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return HetznerDataBase( + id: fields[1] as int, + name: fields[2] as String, + ); + } + + @override + void write(BinaryWriter writer, HetznerDataBase obj) { + writer + ..writeByte(2) + ..writeByte(1) + ..write(obj.id) + ..writeByte(2) + ..write(obj.name); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HetznerDataBaseAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/logic/models/service.dart b/lib/logic/models/service.dart deleted file mode 100644 index 8ce6e310..00000000 --- a/lib/logic/models/service.dart +++ /dev/null @@ -1,25 +0,0 @@ -// import 'package:equatable/equatable.dart'; -// import 'package:selfprivacy/logic/models/state_types.dart'; - -// enum ServiceTypes { -// messanger, -// mail, -// passwordManager, -// github, -// cloud, -// } - -// class Service extends Equatable { -// const Service({required this.state, required this.type}); - -// final StateType state; -// final ServiceTypes type; - -// Service updateState(StateType newState) => Service( -// state: newState, -// type: type, -// ); - -// @override -// List get props => [state, type]; -// } diff --git a/lib/main.dart b/lib/main.dart index 4d678e31..231a7dca 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,12 +15,11 @@ import 'config/get_it_config.dart'; import 'config/localization.dart'; import 'logic/cubit/app_settings/app_settings_cubit.dart'; - void main() async { await HiveConfig.init(); Bloc.observer = SimpleBlocObserver(); Wakelock.enable(); - getItSetup(); + await getItSetup(); WidgetsFlutterBinding.ensureInitialized(); await EasyLocalization.ensureInitialized(); diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index 192f8a8b..a2de8592 100644 --- a/lib/ui/components/brand_button/brand_button.dart +++ b/lib/ui/components/brand_button/brand_button.dart @@ -34,7 +34,7 @@ class BrandButton { onPressed: onPressed, ); - static iconText({ + static emptyWithIconText({ Key? key, required VoidCallback onPressed, required String title, @@ -100,6 +100,7 @@ class _TextButton extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: onPressed, + behavior: HitTestBehavior.opaque, child: Container( height: 48, width: double.infinity, diff --git a/lib/ui/components/brand_card/brand_card.dart b/lib/ui/components/brand_card/brand_card.dart index 9fcfcb6b..18fd26eb 100644 --- a/lib/ui/components/brand_card/brand_card.dart +++ b/lib/ui/components/brand_card/brand_card.dart @@ -13,7 +13,6 @@ class BrandCard extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.only(bottom: 30), decoration: BoxDecoration( color: Theme.of(context).brightness == Brightness.dark ? BrandColors.black diff --git a/lib/ui/components/brand_text/brand_text.dart b/lib/ui/components/brand_text/brand_text.dart index 9dda8549..1acbb4e8 100644 --- a/lib/ui/components/brand_text/brand_text.dart +++ b/lib/ui/components/brand_text/brand_text.dart @@ -6,6 +6,7 @@ enum TextType { h2, // cards titles h3, // titles in about page h4, // caption + h5, // Table data body1, // normal body2, // with opacity medium, @@ -63,10 +64,28 @@ class BrandText extends StatelessWidget { textAlign: textAlign, overflow: TextOverflow.ellipsis, ); - factory BrandText.h4(String? text, {TextStyle? style}) => BrandText( + factory BrandText.h4( + String? text, { + TextStyle? style, + TextAlign? textAlign, + }) => + BrandText( text, type: TextType.h4, style: style, + textAlign: textAlign, + ); + + factory BrandText.h5( + String? text, { + TextStyle? style, + TextAlign? textAlign, + }) => + BrandText( + text, + type: TextType.h5, + style: style, + textAlign: textAlign, ); factory BrandText.body1(String? text, {TextStyle? style}) => BrandText( text, @@ -123,6 +142,11 @@ class BrandText extends StatelessWidget { ? headline4Style.copyWith(color: Colors.white) : headline4Style; break; + case TextType.h5: + style = isDark + ? headline5Style.copyWith(color: Colors.white) + : headline5Style; + break; case TextType.body1: style = isDark ? body1Style.copyWith(color: Colors.white) : body1Style; break; diff --git a/lib/ui/components/brand_timer/brand_timer.dart b/lib/ui/components/brand_timer/brand_timer.dart index e1395ced..38eeb356 100644 --- a/lib/ui/components/brand_timer/brand_timer.dart +++ b/lib/ui/components/brand_timer/brand_timer.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; import 'package:selfprivacy/utils/named_font_weight.dart'; +import 'package:easy_localization/easy_localization.dart'; class BrandTimer extends StatefulWidget { const BrandTimer({ @@ -11,8 +12,8 @@ class BrandTimer extends StatefulWidget { required this.duration, }) : super(key: key); - final DateTime? startDateTime; - final Duration? duration; + final DateTime startDateTime; + final Duration duration; @override _BrandTimerState createState() => _BrandTimerState(); @@ -31,8 +32,8 @@ class _BrandTimerState extends State { _timerStart() { _timeString = diffenceFromStart; timer = Timer.periodic(Duration(seconds: 1), (Timer t) { - var timePassed = DateTime.now().difference(widget.startDateTime!); - if (timePassed > widget.duration!) { + var timePassed = DateTime.now().difference(widget.startDateTime); + if (timePassed > widget.duration) { t.cancel(); } else { _getTime(); @@ -66,14 +67,14 @@ class _BrandTimerState extends State { } String get diffenceFromStart => - _durationToString(DateTime.now().difference(widget.startDateTime!)); + _durationToString(DateTime.now().difference(widget.startDateTime)); String _durationToString(Duration duration) { String twoDigits(int n) => n.toString().padLeft(2, "0"); String twoDigitSeconds = - twoDigits(widget.duration!.inSeconds - duration.inSeconds.remainder(60)); + twoDigits(widget.duration.inSeconds - duration.inSeconds.remainder(60)); - return "$twoDigitSeconds cек"; + return "timer.sec".tr(args: [twoDigitSeconds]); } @override diff --git a/lib/ui/components/one_page/one_page.dart b/lib/ui/components/one_page/one_page.dart new file mode 100644 index 00000000..7cb27a83 --- /dev/null +++ b/lib/ui/components/one_page/one_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_divider/brand_divider.dart'; +import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/ui/components/pre_styled_buttons.dart'; + +class OnePage extends StatelessWidget { + const OnePage({ + Key? key, + required this.title, + required this.child, + }) : super(key: key); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + return SafeArea( + child: 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()), + ), + ), + ), + ), + ); + } +} diff --git a/lib/ui/components/pre_styled_buttons.dart b/lib/ui/components/pre_styled_buttons.dart new file mode 100644 index 00000000..9a5e8baf --- /dev/null +++ b/lib/ui/components/pre_styled_buttons.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class PreStyledButtons { + static Widget close({ + required VoidCallback onPress, + }) => + _CloseButton(onPress: onPress); +} + +class _CloseButton extends StatelessWidget { + const _CloseButton({Key? key, required this.onPress}) : super(key: key); + + 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), + ], + ), + ); + } +} diff --git a/lib/ui/components/progress_bar/progress_bar.dart b/lib/ui/components/progress_bar/progress_bar.dart index 27033239..a75c44fa 100644 --- a/lib/ui/components/progress_bar/progress_bar.dart +++ b/lib/ui/components/progress_bar/progress_bar.dart @@ -47,25 +47,27 @@ class _ProgressBarState extends State { i++; } - odd - ..insert( - 0, - SizedBox( - width: 20, - ), - ) - ..add( - SizedBox( - width: 10, - ), - ); + odd.insert( + 0, + SizedBox( + width: 10, + ), + ); + even.add( + SizedBox( + width: 10, + ), + ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ BrandText.h2('Progress'), SizedBox(height: 10), - Row(children: even), + Row( + children: even, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), SizedBox(height: 7), Container( alignment: Alignment.centerLeft, @@ -96,12 +98,13 @@ class _ProgressBarState extends State { SizedBox(height: 5), Row( children: odd, + mainAxisAlignment: MainAxisAlignment.spaceBetween, ), ], ); } - Expanded _stepTitle({ + Container _stepTitle({ required int index, TextStyle? style, String? step, @@ -110,17 +113,19 @@ class _ProgressBarState extends State { var checked = index < widget.activeIndex; style = isActive ? style!.copyWith(fontWeight: FontWeight.w700) : style; - return Expanded( - flex: 2, + return Container( + padding: EdgeInsets.only(left: 10), + height: 20, + alignment: Alignment.center, child: RichText( - textAlign: TextAlign.center, + textAlign: TextAlign.justify, text: TextSpan( style: progressTextStyleLight, children: [ checked ? WidgetSpan( child: Padding( - padding: const EdgeInsets.only(bottom: 0, right: 2), + padding: const EdgeInsets.only(bottom: 2, right: 2), child: Icon(BrandIcons.check, size: 11), )) : TextSpan(text: '${index + 1}.', style: style), diff --git a/lib/ui/pages/initializing/initializing.dart b/lib/ui/pages/initializing/initializing.dart index 0f914a6b..3b966c9e 100644 --- a/lib/ui/pages/initializing/initializing.dart +++ b/lib/ui/pages/initializing/initializing.dart @@ -34,6 +34,7 @@ class InitializingPage extends StatelessWidget { () => _stepCheck(cubit), () => _stepCheck(cubit), () => _stepCheck(cubit), + () => _stepCheck(cubit), () => Container(child: Text('Everythigng is initialized')) ][cubit.state.progress](); return BlocListener( @@ -47,25 +48,21 @@ class InitializingPage extends StatelessWidget { body: ListView( children: [ Padding( - padding: brandPagePadding1, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ProgressBar( - steps: [ - 'Hetzner', - 'CloudFlare', - 'Backblaze', - 'Domain', - 'User', - 'Server', - ' ✅', - ' ✅', - ' ✅' - ], - activeIndex: cubit.state.progress, - ), + padding: brandPagePadding2.copyWith(top: 10, bottom: 10), + child: ProgressBar( + steps: [ + 'Hetzner', + 'CloudFlare', + 'Backblaze', + 'Domain', + 'User', + 'Server', + ' ✅', + ' ✅', + ' ✅', + ' ✅', ], + activeIndex: cubit.state.progress, ), ), _addCard( @@ -77,14 +74,13 @@ class InitializingPage extends StatelessWidget { BrandButton.text( title: cubit.state.isFullyInitilized ? 'basis.close'.tr() - : 'Настрою потом', + : 'basis.later'.tr(), onPressed: () { Navigator.of(context).pushAndRemoveUntil( materialRoute(RootPage()), (predicate) => false, ); }), - SizedBox(height: 30), ], ), ), @@ -408,14 +404,15 @@ class InitializingPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Spacer(flex: 2), - BrandText.h2('initializing.how'.tr()), + BrandText.h2('initializing.final'.tr()), SizedBox(height: 10), BrandText.body2('initializing.11'.tr()), Spacer(), BrandButton.rised( - onPressed: - isLoading! ? null : appConfigCubit.createServerAndSetDnsRecords, - title: isLoading ? 'loading' : 'initializing.11'.tr(), + onPressed: isLoading! + ? null + : () => appConfigCubit.createServerAndSetDnsRecords(), + title: isLoading ? 'basis.loading'.tr() : 'initializing.11'.tr(), ), Spacer(flex: 2), BrandButton.text( @@ -431,10 +428,12 @@ class InitializingPage extends StatelessWidget { assert(appConfigCubit.state is TimerState, 'wronge state'); var state = appConfigCubit.state as TimerState; - String? text; - if (state.isServerReseted!) { + late String? text; + if (state.isServerResetedSecondTime) { text = 'initializing.13'.tr(); - } else if (state.isServerStarted!) { + } else if (state.isServerResetedFirstTime) { + text = 'initializing.21'.tr(); + } else if (state.isServerStarted) { text = 'initializing.14'.tr(); } else if (state.isServerCreated) { text = 'initializing.15'.tr(); @@ -452,8 +451,8 @@ class InitializingPage extends StatelessWidget { children: [ BrandText.body2('initializing.16'.tr()), BrandTimer( - startDateTime: state.timerStart, - duration: state.duration, + startDateTime: state.timerStart!, + duration: state.duration!, ) ], ), @@ -472,7 +471,7 @@ class InitializingPage extends StatelessWidget { Widget _addCard(Widget child) { return Container( - height: 500, + height: 450, padding: brandPagePadding2, child: BrandCard(child: child), ); diff --git a/lib/ui/pages/more/app_settings/app_setting.dart b/lib/ui/pages/more/app_settings/app_setting.dart index 8d3d33c1..9e89d7dc 100644 --- a/lib/ui/pages/more/app_settings/app_setting.dart +++ b/lib/ui/pages/more/app_settings/app_setting.dart @@ -28,7 +28,7 @@ class _AppSettingsPageState extends State { return Scaffold( appBar: PreferredSize( child: - BrandHeader(title: 'more.settings'.tr(), hasBackButton: true), + BrandHeader(title: 'more.settings.title'.tr(), hasBackButton: true), preferredSize: Size.fromHeight(52), ), body: ListView( @@ -47,8 +47,8 @@ class _AppSettingsPageState extends State { children: [ Flexible( child: _TextColumn( - title: 'Dark Theme', - value: 'Change your the app theme', + title: 'more.settings.1'.tr(), + value: 'more.settings.2'.tr(), ), ), SizedBox(width: 5), @@ -75,8 +75,8 @@ class _AppSettingsPageState extends State { children: [ Flexible( child: _TextColumn( - title: 'Reset app config', - value: 'Reset api keys and root user', + title: 'more.settings.3'.tr(), + value: 'more.settings.4'.tr(), ), ), SizedBox(width: 5), @@ -85,7 +85,7 @@ class _AppSettingsPageState extends State { primary: BrandColors.red1, ), child: Text( - 'Reset', + 'basis.reset'.tr(), style: TextStyle( color: BrandColors.white, fontWeight: NamedFontWeight.demiBold, @@ -96,11 +96,11 @@ class _AppSettingsPageState extends State { context: context, builder: (_) { return BrandAlert( - title: 'Вы уверенны', - contentText: 'Сбросить все ключи?', + title: 'modals.3'.tr(), + contentText: 'modals.4'.tr(), acitons: [ ActionButton( - text: 'Да, сбросить', + text: 'modals.5'.tr(), isRed: true, onPressed: () { context @@ -109,7 +109,7 @@ class _AppSettingsPageState extends State { Navigator.of(context).pop(); }), ActionButton( - text: 'Отмена', + text: 'basis.cancel'.tr(), ), ], ); diff --git a/lib/ui/pages/more/more.dart b/lib/ui/pages/more/more.dart index 8959d87a..112b8c64 100644 --- a/lib/ui/pages/more/more.dart +++ b/lib/ui/pages/more/more.dart @@ -39,7 +39,7 @@ class MorePage extends StatelessWidget { goTo: InitializingPage(), ), _NavItem( - title: 'more.settings'.tr(), + title: 'more.settings.title'.tr(), iconData: BrandIcons.settings, goTo: AppSettingsPage(), ), diff --git a/lib/ui/pages/onboarding/onboarding.dart b/lib/ui/pages/onboarding/onboarding.dart index 07790f60..835e40e0 100644 --- a/lib/ui/pages/onboarding/onboarding.dart +++ b/lib/ui/pages/onboarding/onboarding.dart @@ -124,8 +124,10 @@ class _OnboardingPageState extends State { BrandButton.rised( onPressed: () { context.read().turnOffOnboarding(); - Navigator.of(context) - .pushReplacement(materialRoute(widget.nextPage)); + Navigator.of(context).pushAndRemoveUntil( + materialRoute(widget.nextPage), + (route) => false, + ); }, title: 'basis.got_it'.tr(), ), diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index f4db4a53..db953281 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -10,9 +10,14 @@ import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.da import 'package:selfprivacy/ui/components/brand_text/brand_text.dart'; 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:selfprivacy/ui/pages/providers/settings/settings.dart'; +import 'package:selfprivacy/ui/components/one_page/one_page.dart'; +import 'package:selfprivacy/ui/pages/server_details/server_details.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:selfprivacy/utils/route_transitions/slide_bottom.dart'; +import 'package:selfprivacy/utils/ui_helpers.dart'; + +var navigatorKey = GlobalKey(); class ProvidersPage extends StatefulWidget { ProvidersPage({Key? key}) : super(key: key); @@ -27,9 +32,17 @@ class _ProvidersPageState extends State { var isReady = context.watch().state.isFullyInitilized; final cards = ProviderType.values - .map((type) => _Card( - provider: - ProviderModel(state: StateType.uninitialized, type: type))) + .map( + (type) => Padding( + padding: EdgeInsets.only(bottom: 30), + child: _Card( + provider: ProviderModel( + state: isReady ? StateType.stable : StateType.uninitialized, + type: type, + ), + ), + ), + ) .toList(); return Scaffold( appBar: PreferredSize( @@ -56,9 +69,11 @@ class _Card extends StatelessWidget { final ProviderModel provider; @override Widget build(BuildContext context) { - String? title; + late String title; String? message; - String? stableText; + late String stableText; + late VoidCallback onTap; + AppConfigState appConfig = context.watch().state; var domainName = @@ -67,30 +82,54 @@ class _Card extends StatelessWidget { switch (provider.type) { case ProviderType.server: title = 'providers.server.card_title'.tr(); - stableText = 'В норме'; + stableText = 'providers.domain.status'.tr(); + + stableText = 'providers.server.status'.tr(); + onTap = () => Navigator.of(context).push( + SlideBottomRoute( + OnePage( + title: title, + child: ServerDetails(), + ), + ), + ); break; case ProviderType.domain: title = 'providers.domain.card_title'.tr(); message = domainName; - stableText = 'Домен настроен'; + stableText = 'providers.domain.status'.tr(); + + onTap = () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return _ProviderDetails( + provider: provider, + statusText: stableText, + ); + }, + ); break; case ProviderType.backup: title = 'providers.backup.card_title'.tr(); - stableText = 'В норме'; + stableText = 'providers.backup.status'.tr(); + + onTap = () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return _ProviderDetails( + provider: provider, + statusText: stableText, + ); + }, + ); break; } return GestureDetector( - onTap: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return _ProviderDetails( - provider: provider, - statusText: stableText, - ); - }, - ), + onTap: onTap, child: BrandCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -131,20 +170,11 @@ class _ProviderDetails extends StatelessWidget { var config = context.watch().state; - var domainName = config.isDomainFilled - ? config.cloudFlareDomain!.domainName! - : 'example.com'; + var domainName = UiHelpers.getDomainName(config); + switch (provider.type) { case ProviderType.server: - title = 'providers.server.card_title'.tr(); - children = [ - BrandText.body1('providers.server.bottom_sheet.1'.tr()), - SizedBox(height: 10), - BrandText.body1('providers.server.bottom_sheet.2'.tr()), - SizedBox(height: 10), - BrandText.body1('providers.server.bottom_sheet.3'.tr()), - ]; - break; + throw ('wrong type'); case ProviderType.domain: title = 'providers.domain.card_title'.tr(); children = [ @@ -153,7 +183,7 @@ class _ProviderDetails extends StatelessWidget { BrandText.body1( 'providers.domain.bottom_sheet.2'.tr(args: [domainName, 'Date'])), SizedBox(height: 10), - BrandText.body1('providers.domain.bottom_sheet.3'.tr()), + BrandText.body1('providers.domain.status'.tr()), ]; break; case ProviderType.backup: @@ -164,7 +194,7 @@ class _ProviderDetails extends StatelessWidget { BrandText.body1( 'providers.backup.bottom_sheet.2'.tr(args: [domainName, 'Time'])), SizedBox(height: 10), - BrandText.body1('providers.backup.bottom_sheet.3'.tr()), + BrandText.body1('providers.backup.status'.tr()), ]; break; } @@ -177,38 +207,7 @@ class _ProviderDetails extends StatelessWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: 4, - horizontal: 2, - ), - child: PopupMenuButton<_PopupMenuItemType>( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - ), - onSelected: (_PopupMenuItemType result) { - switch (result) { - case _PopupMenuItemType.setting: - navigatorKey.currentState! - .push(materialRoute(SettingsPage())); - 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()), - ), - ), - ], - ), - ), - ), + SizedBox(height: 40), Padding( padding: brandPagePadding2, child: Column( @@ -234,5 +233,3 @@ class _ProviderDetails extends StatelessWidget { ); } } - -enum _PopupMenuItemType { setting } diff --git a/lib/ui/pages/server_details/server_details.dart b/lib/ui/pages/server_details/server_details.dart new file mode 100644 index 00000000..3e578199 --- /dev/null +++ b/lib/ui/pages/server_details/server_details.dart @@ -0,0 +1,287 @@ +import 'package:cubit_form/cubit_form.dart'; +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_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/server_detailed_info/server_detailed_info_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_icons/brand_icons.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/utils/named_font_weight.dart'; + +part 'server_settings.dart'; + +var navigatorKey = GlobalKey(); + +class ServerDetails extends StatefulWidget { + const ServerDetails({Key? key}) : super(key: key); + + @override + _ServerDetailsState createState() => _ServerDetailsState(); +} + +class _ServerDetailsState extends State + with SingleTickerProviderStateMixin { + late TabController tabController; + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } + + @override + void initState() { + tabController = TabController(length: 2, vsync: this); + tabController.addListener(() { + setState(() {}); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var isReady = context.watch().state.isFullyInitilized; + var providerState = isReady ? StateType.stable : StateType.uninitialized; + + late String title = 'providers.server.card_title'.tr(); + + return TabBarView( + physics: NeverScrollableScrollPhysics(), + controller: tabController, + children: [ + BlocProvider( + create: (context) => ServerDetailsCubit()..check(), + child: Builder(builder: (context) { + var 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; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: brandPagePadding2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + IconStatusMask( + status: providerState, + child: Icon( + BrandIcons.server, + size: 40, + color: Colors.white, + ), + ), + SizedBox(width: 10), + BrandText.h2(title), + Spacer(), + Padding( + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: 2, + ), + child: PopupMenuButton<_PopupMenuItemType>( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10.0), + ), + 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()), + ), + ), + ], + ), + ), + ], + ), + SizedBox(height: 10), + BrandText.body1('providers.server.bottom_sheet.1'.tr()), + SizedBox(height: 30), + Center(child: BrandText.h2('providers.server.2'.tr())), + SizedBox(height: 10), + Table( + columnWidths: { + 0: FractionColumnWidth(.5), + 1: FractionColumnWidth(.5), + }, + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + getRowTitle('Last check'), + getRowValue(formater.format(checkTime)), + ], + ), + TableRow( + children: [ + getRowTitle('Server Id'), + getRowValue(data.id.toString()), + ], + ), + TableRow( + children: [ + getRowTitle('Status:'), + getRowValue( + '${data.status.toString().split('.')[1].toUpperCase()}', + isBold: true, + ), + ], + ), + TableRow( + children: [ + getRowTitle('CPU'), + getRowValue( + data.serverType.cores.toString(), + ), + ], + ), + TableRow( + children: [ + getRowTitle('Memory'), + getRowValue( + '${data.serverType.memory.toString()} GB', + ), + ], + ), + TableRow( + children: [ + getRowTitle('Disk Local'), + getRowValue( + '${data.serverType.disk.toString()} GB', + ), + ], + ), + TableRow( + children: [ + getRowTitle('Price monthly:'), + getRowValue( + '${data.serverType.prices[1].monthly.toString()}', + ), + ], + ), + TableRow( + children: [ + getRowTitle('Price hourly:'), + getRowValue( + '${data.serverType.prices[1].hourly.toString()}', + ), + ], + ), + ], + ), + SizedBox(height: 30), + Center(child: BrandText.h2('providers.server.3'.tr())), + SizedBox(height: 10), + Table( + columnWidths: { + 0: FractionColumnWidth(.5), + 1: FractionColumnWidth(.5), + }, + defaultVerticalAlignment: + TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + getRowTitle('Country'), + getRowValue( + '${data.location.country}', + ), + ], + ), + TableRow( + children: [ + getRowTitle('City'), + getRowValue(data.location.city), + ], + ), + TableRow( + children: [ + getRowTitle('Description'), + getRowValue(data.location.description), + ], + ), + ], + ), + ], + ), + ), + ], + ); + } else { + throw Exception('wrong state'); + } + }), + ), + _ServerSettings(tabController: tabController), + ], + ); + } + + Widget getRowTitle(String title) { + return 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, + ); + } +} + +enum _PopupMenuItemType { setting } + +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), + ), + ); + } +} + +final DateFormat formater = DateFormat('HH:mm:ss'); diff --git a/lib/ui/pages/server_details/server_settings.dart b/lib/ui/pages/server_details/server_settings.dart new file mode 100644 index 00000000..b5d48e08 --- /dev/null +++ b/lib/ui/pages/server_details/server_settings.dart @@ -0,0 +1,136 @@ +part of 'server_details.dart'; + +class _ServerSettings extends StatelessWidget { + const _ServerSettings({ + Key? key, + required this.tabController, + }) : super(key: key); + + final TabController tabController; + + @override + Widget build(BuildContext context) { + return ListView( + padding: brandPagePadding2, + children: [ + 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()), + ], + ), + ), + ), + BrandDivider(), + SwitcherBlock( + onChange: (_) {}, + child: _TextColumn( + title: 'Allow Auto-upgrade', + value: 'Wether to allow automatic packages upgrades', + ), + isActive: true, + ), + SwitcherBlock( + onChange: (_) {}, + child: _TextColumn( + title: 'Reboot after upgrade', + value: 'Reboot without prompt after applying updates', + ), + isActive: false, + ), + _Button( + onTap: () {}, + child: _TextColumn( + title: 'Server Timezone', + value: 'Europe/Kyiv', + ), + ), + _Button( + onTap: () {}, + child: _TextColumn( + title: 'Server Locale', + value: 'Default', + ), + ), + _Button( + onTap: () {}, + child: _TextColumn( + hasWarning: true, + title: 'Factory Reset', + value: 'Restore default settings on your server', + ), + ) + ], + ); + } +} + +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( + border: Border( + 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, + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/services/services.dart b/lib/ui/pages/services/services.dart index ef2284a8..c21b0d72 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -4,15 +4,15 @@ 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/models/state_types.dart'; +import 'package:selfprivacy/ui/components/brand_button/brand_button.dart'; import 'package:selfprivacy/ui/components/brand_card/brand_card.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_modal_sheet/brand_modal_sheet.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:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:selfprivacy/utils/route_transitions/basic.dart'; +import 'package:selfprivacy/utils/ui_helpers.dart'; import 'package:url_launcher/url_launcher.dart'; import '../rootRoute.dart'; @@ -40,7 +40,14 @@ class _ServicesPageState extends State { BrandText.body1('services.title'.tr()), SizedBox(height: 24), if (!isReady) ...[NotReadyCard(), SizedBox(height: 24)], - ...ServiceTypes.values.map((t) => _Card(serviceType: t)).toList() + ...ServiceTypes.values + .map((t) => Padding( + padding: EdgeInsets.only( + bottom: 30, + ), + child: _Card(serviceType: t), + )) + .toList() ], ), ); @@ -98,10 +105,10 @@ class _Card extends StatelessWidget { var isReady = context.watch().state.isFullyInitilized; var changeTab = context.read().onPress; return GestureDetector( - onTap: () => showModalBottomSheet( + onTap: () => showDialog( context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, + // isScrollControlled: true, + // backgroundColor: Colors.transparent, builder: (BuildContext context) { return _ServiceDetails( serviceType: serviceType, @@ -163,9 +170,7 @@ class _ServiceDetails extends StatelessWidget { late Widget child; var config = context.watch().state; - var domainName = config.isDomainFilled - ? config.cloudFlareDomain!.domainName! - : 'example.com'; + var domainName = UiHelpers.getDomainName(config); var linksStyle = body1Style.copyWith( fontSize: 15, @@ -174,7 +179,6 @@ class _ServiceDetails extends StatelessWidget { : BrandColors.black, fontWeight: FontWeight.bold, decoration: TextDecoration.underline, - // height: 1.1, ); var textStyle = body1Style.copyWith( @@ -191,9 +195,10 @@ class _ServiceDetails extends StatelessWidget { 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, left: 5), + padding: EdgeInsets.only(bottom: 0.8), child: GestureDetector( child: Text( 'services.mail.bottom_sheet.2'.tr(), @@ -229,9 +234,10 @@ class _ServiceDetails extends StatelessWidget { .tr(args: [domainName]), style: textStyle, ), + WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8, left: 5), + padding: EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://password.$domainName'), child: Text( @@ -252,9 +258,10 @@ class _ServiceDetails extends StatelessWidget { 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, left: 5), + padding: EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://meet.$domainName'), child: Text( @@ -275,9 +282,10 @@ class _ServiceDetails extends StatelessWidget { 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, left: 5), + padding: EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://cloud.$domainName'), child: Text( @@ -299,11 +307,12 @@ class _ServiceDetails extends StatelessWidget { .tr(args: [domainName]), style: textStyle, ), + WidgetSpan(child: SizedBox(width: 5)), WidgetSpan( child: Padding( - padding: EdgeInsets.only(bottom: 0.8, left: 5), + padding: EdgeInsets.only(bottom: 0.8), child: GestureDetector( - onTap: () => _launchURL('https://social_network.$domainName'), + onTap: () => _launchURL('https://social.$domainName'), child: Text( 'social.$domainName', style: linksStyle, @@ -322,9 +331,10 @@ class _ServiceDetails extends StatelessWidget { 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, left: 5), + padding: EdgeInsets.only(bottom: 0.8), child: GestureDetector( onTap: () => _launchURL('https://git.$domainName'), child: Text( @@ -338,39 +348,312 @@ class _ServiceDetails extends StatelessWidget { )); break; } - return BrandModalSheet( - child: Navigator( - key: navigatorKey, - initialRoute: '/', - onGenerateRoute: (_) { - return materialRoute( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: brandPagePadding1, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 13), - IconStatusMask( - status: status, - child: Icon(icon, size: 40, color: Colors.white), + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: SingleChildScrollView( + child: Container( + width: 350, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: brandPagePadding1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IconStatusMask( + status: status, + child: Icon(icon, size: 40, color: Colors.white), + ), + SizedBox(height: 10), + BrandText.h2(title), + SizedBox(height: 10), + child, + SizedBox(height: 40), + Center( + child: Container( + child: BrandButton.rised( + onPressed: () => Navigator.of(context).pop(), + title: 'basis.close'.tr(), + ), ), - SizedBox(height: 10), - BrandText.h1(title), - child, - ], - ), - ) - ], - ), - ); - }, + ), + ], + ), + ) + ], + ), + ), ), ); } - void _launchURL(url) async => - await canLaunch(url) ? await launch(url) : throw 'Could not launch $url'; + void _launchURL(url) async { + var _possible = await canLaunch(url); + + if (_possible) { + try { + await launch( + url, + forceSafariVC: true, + enableJavaScript: true, + ); + } catch (e) { + print(e); + } + } else { + throw 'Could not launch $url'; + } + } } + + +// 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; +// final StateType status; +// final String title; +// final ValueChanged changeTab; + +// @override +// Widget build(BuildContext context) { +// late Widget child; + +// var config = context.watch().state; +// var domainName = UiHelpers.getDomainName(config); + +// var linksStyle = body1Style.copyWith( +// fontSize: 15, +// color: Theme.of(context).brightness == Brightness.dark +// ? Colors.white +// : BrandColors.black, +// fontWeight: FontWeight.bold, +// decoration: TextDecoration.underline, +// // height: 1.1, +// ); + +// var textStyle = body1Style.copyWith( +// color: Theme.of(context).brightness == Brightness.dark +// ? Colors.white +// : BrandColors.black, +// ); +// switch (serviceType) { +// case ServiceTypes.mail: +// child = RichText( +// text: TextSpan( +// children: [ +// TextSpan( +// text: 'services.mail.bottom_sheet.1'.tr(args: [domainName]), +// style: textStyle, +// ), +// WidgetSpan( +// child: Padding( +// padding: EdgeInsets.only(bottom: 0.8, left: 5), +// child: GestureDetector( +// child: Text( +// 'services.mail.bottom_sheet.2'.tr(), +// style: linksStyle, +// ), +// 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, +// ) +// ], +// )); +// break; +// case ServiceTypes.passwordManager: +// child = RichText( +// text: TextSpan( +// children: [ +// TextSpan( +// text: 'services.password_manager.bottom_sheet.1' +// .tr(args: [domainName]), +// style: textStyle, +// ), +// WidgetSpan( +// child: Padding( +// padding: EdgeInsets.only(bottom: 0.8, left: 5), +// 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: Padding( +// padding: EdgeInsets.only(bottom: 0.8, left: 5), +// 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: Padding( +// padding: EdgeInsets.only(bottom: 0.8, left: 5), +// 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: Padding( +// padding: EdgeInsets.only(bottom: 0.8, left: 5), +// 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: Padding( +// padding: EdgeInsets.only(bottom: 0.8, left: 5), +// child: GestureDetector( +// onTap: () => _launchURL('https://git.$domainName'), +// child: Text( +// 'git.$domainName', +// style: linksStyle, +// ), +// ), +// ), +// ), +// ], +// )); +// break; +// } +// return BrandModalSheet( + // child: Navigator( + // key: navigatorKey, + // initialRoute: '/', + // onGenerateRoute: (_) { + // return materialRoute( +// Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Padding( +// padding: brandPagePadding1, +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// SizedBox(height: 13), +// IconStatusMask( +// status: status, +// child: Icon(icon, size: 40, color: Colors.white), +// ), +// SizedBox(height: 10), +// BrandText.h1(title), +// SizedBox(height: 10), +// child, +// ], +// ), +// ) +// ], +// ), +// ); +// }, +// ), +// ); +// } + +// void _launchURL(url) async { +// var _possible = await canLaunch(url); + +// if (_possible) { +// try { +// await launch( +// url, +// forceSafariVC: true, +// enableJavaScript: true, +// ); +// } catch (e) { +// print(e); +// } +// } else { +// throw 'Could not launch $url'; +// } +// } +// } + diff --git a/lib/ui/pages/users/new_user.dart b/lib/ui/pages/users/new_user.dart index 53636e74..62eb624f 100644 --- a/lib/ui/pages/users/new_user.dart +++ b/lib/ui/pages/users/new_user.dart @@ -5,14 +5,12 @@ class _NewUser extends StatelessWidget { Widget build(BuildContext context) { var config = context.watch().state; - var domainName = config.isDomainFilled - ? config.cloudFlareDomain!.domainName! - : 'example.com'; + var domainName = UiHelpers.getDomainName(config); return BrandModalSheet( child: BlocProvider( create: (context) => - UserFormCubit(usersCubit: context.watch()), + UserFormCubit(usersCubit: context.read()), child: Builder(builder: (context) { var formCubitState = context.watch().state; diff --git a/lib/ui/pages/users/user_details.dart b/lib/ui/pages/users/user_details.dart index 99f43315..3c788bec 100644 --- a/lib/ui/pages/users/user_details.dart +++ b/lib/ui/pages/users/user_details.dart @@ -12,9 +12,7 @@ class _UserDetails extends StatelessWidget { Widget build(BuildContext context) { var config = context.watch().state; - var domainName = config.isDomainFilled - ? config.cloudFlareDomain!.domainName! - : 'example.com'; + var domainName = UiHelpers.getDomainName(config); return BrandModalSheet( child: Column( @@ -44,8 +42,8 @@ class _UserDetails extends StatelessWidget { ), onSelected: (PopupMenuItemType result) { switch (result) { - case PopupMenuItemType.reset: - break; + // case PopupMenuItemType.reset: + // break; case PopupMenuItemType.delete: showDialog( context: context, @@ -88,13 +86,13 @@ class _UserDetails extends StatelessWidget { }, 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()), - ), - ), + // PopupMenuItem( + // value: PopupMenuItemType.reset, + // child: Container( + // padding: EdgeInsets.only(left: 5), + // child: Text('users.reset_password'.tr()), + // ), + // ), PopupMenuItem( value: PopupMenuItemType.delete, child: Container( @@ -145,7 +143,7 @@ class _UserDetails extends StatelessWidget { SizedBox(height: 24), BrandDivider(), SizedBox(height: 20), - BrandButton.iconText( + BrandButton.emptyWithIconText( title: 'users.send_regisration_data'.tr(), icon: Icon(BrandIcons.share), onPressed: () {}, @@ -161,6 +159,6 @@ class _UserDetails extends StatelessWidget { } enum PopupMenuItemType { - reset, + // reset, delete, } diff --git a/lib/ui/pages/users/users.dart b/lib/ui/pages/users/users.dart index 8acb4016..271182e6 100644 --- a/lib/ui/pages/users/users.dart +++ b/lib/ui/pages/users/users.dart @@ -14,6 +14,7 @@ import 'package:selfprivacy/ui/components/brand_modal_sheet/brand_modal_sheet.da import 'package:selfprivacy/ui/components/brand_text/brand_text.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'; part 'fab.dart'; part 'new_user.dart'; diff --git a/lib/utils/password_generator2.dart b/lib/utils/password_generator2.dart index cea7895c..36114d55 100644 --- a/lib/utils/password_generator2.dart +++ b/lib/utils/password_generator2.dart @@ -4,11 +4,11 @@ const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890_'; Random _rnd = Random(); -String getRandomString(int length) => String.fromCharCodes( +String getRandomString(int length, [chars = _chars]) => String.fromCharCodes( Iterable.generate( length, - (_) => _chars.codeUnitAt( - _rnd.nextInt(_chars.length), + (_) => chars.codeUnitAt( + _rnd.nextInt(chars.length), ), ), ); diff --git a/lib/utils/route_transitions/slide_bottom.dart b/lib/utils/route_transitions/slide_bottom.dart index 607ab105..380b1142 100644 --- a/lib/utils/route_transitions/slide_bottom.dart +++ b/lib/utils/route_transitions/slide_bottom.dart @@ -36,8 +36,10 @@ Function transitionsBuilder = ( class SlideBottomRoute extends PageRouteBuilder { SlideBottomRoute(this.widget) : super( + transitionDuration: Duration(milliseconds: 150), pageBuilder: pageBuilder(widget), - transitionsBuilder: transitionsBuilder as Widget Function(BuildContext, Animation, Animation, Widget), + transitionsBuilder: transitionsBuilder as Widget Function( + BuildContext, Animation, Animation, Widget), ); final Widget widget; diff --git a/lib/utils/ui_helpers.dart b/lib/utils/ui_helpers.dart new file mode 100644 index 00000000..b0b43851 --- /dev/null +++ b/lib/utils/ui_helpers.dart @@ -0,0 +1,9 @@ +import 'package:selfprivacy/logic/cubit/app_config/app_config_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'; +} diff --git a/pubspec.lock b/pubspec.lock index 088143aa..7be23a73 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "14.0.0" + version: "19.0.0" analyzer: dependency: transitive description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.41.2" + version: "1.3.0" archive: dependency: transitive description: @@ -42,7 +42,7 @@ packages: name: basic_utils url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.1" + version: "3.0.0-nullsafety.3" bloc: dependency: transitive description: @@ -63,14 +63,14 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "2.0.0" build_config: dependency: transitive description: name: build_config url: "https://pub.dartlang.org" source: hosted - version: "0.4.5" + version: "0.4.7" build_daemon: dependency: transitive description: @@ -84,35 +84,35 @@ packages: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "1.5.3" + version: "2.0.0" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "1.11.5" + version: "1.12.2" build_runner_core: dependency: transitive description: name: build_runner_core url: "https://pub.dartlang.org" source: hosted - version: "6.1.10" + version: "6.1.12" built_collection: dependency: transitive description: name: built_collection url: "https://pub.dartlang.org" source: hosted - version: "4.3.2" + version: "5.0.0" built_value: dependency: transitive description: name: built_value url: "https://pub.dartlang.org" source: hosted - version: "7.1.0" + version: "8.0.4" characters: dependency: transitive description: @@ -133,14 +133,14 @@ packages: name: checked_yaml url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "2.0.1" cli_util: dependency: transitive description: name: cli_util url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.3.0" clock: dependency: transitive description: @@ -154,7 +154,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.7.0" collection: dependency: transitive description: @@ -175,7 +175,7 @@ packages: name: coverage url: "https://pub.dartlang.org" source: hosted - version: "0.15.2" + version: "1.0.2" crypto: dependency: "direct main" description: @@ -189,7 +189,7 @@ packages: name: cubit_form url: "https://pub.dartlang.org" source: hosted - version: "1.0.0-nullsafety.0" + version: "1.0.2-nullsafety.0" cupertino_icons: dependency: "direct main" description: @@ -203,14 +203,14 @@ packages: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "1.3.12" + version: "2.0.0" dio: dependency: "direct main" description: name: dio url: "https://pub.dartlang.org" source: hosted - version: "4.0.0-prev1" + version: "4.0.0" easy_localization: dependency: "direct main" description: @@ -273,7 +273,7 @@ packages: name: fixnum url: "https://pub.dartlang.org" source: hosted - version: "0.10.11" + version: "1.0.0" flutter: dependency: "direct main" description: flutter @@ -304,7 +304,7 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.6.0" + version: "0.6.1" flutter_secure_storage: dependency: "direct main" description: @@ -335,21 +335,21 @@ packages: name: glob url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" graphs: dependency: transitive description: name: graphs url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "1.0.0" hive: dependency: "direct main" description: name: hive url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.1" hive_flutter: dependency: "direct main" description: @@ -357,20 +357,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.13.0" + version: "0.13.1" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "3.0.0" http_parser: dependency: transitive description: @@ -384,7 +391,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.0.2" intl: dependency: transitive description: @@ -398,7 +405,7 @@ packages: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "1.0.0" js: dependency: transitive description: @@ -412,21 +419,21 @@ packages: name: json_annotation url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.0.1" json_serializable: dependency: "direct dev" description: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "4.0.2" + version: "4.1.0" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" markdown: dependency: transitive description: @@ -461,7 +468,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.7" + version: "1.0.0" nested: dependency: transitive description: @@ -482,7 +489,7 @@ packages: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "2.0.0" package_info: dependency: "direct main" description: @@ -566,7 +573,7 @@ packages: name: pointycastle url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.2" + version: "3.0.1" pool: dependency: transitive description: @@ -574,13 +581,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0-beta-1" process: dependency: transitive description: name: process url: "https://pub.dartlang.org" source: hosted - version: "4.1.0" + version: "4.2.1" provider: dependency: "direct main" description: @@ -594,21 +608,14 @@ packages: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "2.0.0" pubspec_parse: dependency: transitive description: name: pubspec_parse url: "https://pub.dartlang.org" source: hosted - version: "0.1.7" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.5" + version: "1.0.0" shared_preferences: dependency: transitive description: @@ -664,21 +671,21 @@ packages: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.0" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.9+2" + version: "1.0.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.4+1" + version: "1.0.1" sky_engine: dependency: transitive description: flutter @@ -690,7 +697,7 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "0.9.10+3" + version: "1.0.0" source_map_stack_trace: dependency: transitive description: @@ -732,7 +739,7 @@ packages: name: stream_transform url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" string_scanner: dependency: transitive description: @@ -774,7 +781,7 @@ packages: name: timing url: "https://pub.dartlang.org" source: hosted - version: "0.1.1+3" + version: "1.0.0" typed_data: dependency: transitive description: @@ -782,13 +789,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + unicons: + dependency: "direct main" + description: + name: unicons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" url_launcher: dependency: "direct main" description: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.2" + version: "6.0.3" url_launcher_linux: dependency: transitive description: @@ -837,7 +851,7 @@ packages: name: vm_service url: "https://pub.dartlang.org" source: hosted - version: "5.5.0" + version: "6.1.0+1" wakelock: dependency: "direct main" description: @@ -879,28 +893,28 @@ packages: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+15" + version: "1.0.0" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "2.0.0" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol url: "https://pub.dartlang.org" source: hosted - version: "0.7.5" + version: "1.0.0" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b891badd..f71c9d79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: selfprivacy description: selfprivacy.org publish_to: 'none' -version: 0.1.0+1 +version: 0.1.0+2 environment: sdk: '>=2.12.0 <3.0.0' @@ -17,7 +17,7 @@ dependencies: easy_localization: ^3.0.0 either_option: ^2.0.1-dev.1 equatable: ^2.0.0 - flutter_bloc: ^7.0.0-nullsafety.5 + flutter_bloc: ^7.0.0 flutter_markdown: ^0.6.0 flutter_secure_storage: ^4.1.0 get_it: ^6.0.0 @@ -25,7 +25,9 @@ dependencies: hive_flutter: ^1.0.0 json_annotation: ^4.0.0 package_info: ^2.0.0 + pretty_dio_logger: ^1.1.1 provider: ^5.0.0 + unicons: ^1.0.2 url_launcher: ^6.0.2 wakelock: ^0.5.0+2 @@ -35,6 +37,7 @@ dev_dependencies: basic_utils: ^3.0.0-nullsafety.1 build_runner: ^1.11.5 flutter_launcher_icons: ^0.9.0 + hive_generator: ^1.0.0 json_serializable: ^4.0.2 flutter_icons: