From 804147b8d6b03c67abf1782d8aca6618cdf5867b Mon Sep 17 00:00:00 2001 From: Kherel Date: Fri, 26 Mar 2021 14:38:39 +0100 Subject: [PATCH] update --- assets/translations/en.json | 19 +- assets/translations/ru.json | 17 +- build.yaml | 7 + lib/config/text_themes.dart | 6 + lib/logic/api_maps/api_map.dart | 28 +- lib/logic/api_maps/backblaze.dart | 4 +- lib/logic/api_maps/cloudflare.dart | 22 +- lib/logic/api_maps/hetzner.dart | 21 +- lib/logic/api_maps/server.dart | 3 +- .../cubit/app_config/app_config_cubit.dart | 2 +- .../cubit/app_config/app_config_state.dart | 1 - .../server_detailed_info_cubit.dart | 24 ++ .../server_detailed_info_repository.dart | 9 + .../server_detailed_info_state.dart | 29 ++ lib/logic/models/backblaze_credential.dart | 6 +- lib/logic/models/backblaze_credential.g.dart | 4 +- lib/logic/models/cloudflare_domain.dart | 9 +- lib/logic/models/cloudflare_domain.g.dart | 4 +- lib/logic/models/hetzner_server_info.dart | 89 +++++ lib/logic/models/hetzner_server_info.g.dart | 84 +++++ lib/logic/models/server_info.dart | 23 -- lib/main.dart | 2 - .../components/brand_button/brand_button.dart | 2 +- lib/ui/components/brand_text/brand_text.dart | 26 +- lib/ui/components/one_page/one_page.dart | 51 +++ lib/ui/components/pre_styled_buttons.dart | 30 ++ lib/ui/pages/providers/providers.dart | 116 +++--- .../pages/server_details/server_details.dart | 290 +++++++++++++++ .../pages/server_details/server_settings.dart | 136 +++++++ lib/ui/pages/services/services.dart | 346 +++++++++++++++--- lib/ui/pages/users/new_user.dart | 6 +- lib/ui/pages/users/user_details.dart | 26 +- lib/ui/pages/users/users.dart | 1 + lib/utils/route_transitions/slide_bottom.dart | 4 +- lib/utils/ui_helpers.dart | 9 + pubspec.lock | 14 + pubspec.yaml | 2 + 37 files changed, 1241 insertions(+), 231 deletions(-) create mode 100644 build.yaml create mode 100644 lib/logic/cubit/server_detailed_info/server_detailed_info_cubit.dart create mode 100644 lib/logic/cubit/server_detailed_info/server_detailed_info_repository.dart create mode 100644 lib/logic/cubit/server_detailed_info/server_detailed_info_state.dart create mode 100644 lib/logic/models/hetzner_server_info.dart create mode 100644 lib/logic/models/hetzner_server_info.g.dart delete mode 100644 lib/logic/models/server_info.dart create mode 100644 lib/ui/components/one_page/one_page.dart create mode 100644 lib/ui/components/pre_styled_buttons.dart create mode 100644 lib/ui/pages/server_details/server_details.dart create mode 100644 lib/ui/pages/server_details/server_settings.dart create mode 100644 lib/utils/ui_helpers.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 3c7a098af2..da4f6a4ac5 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -20,9 +20,11 @@ "domain": "Domain", "saving": "Saving..", "nickname": "nickname", - "loading": "loading", + "loading": "Loading...", "later": "Настрою потом", - "reset": "Reset" + "reset": "Reset", + "details": "Details", + "no_data": "No data" }, "more": { "_comment": "'More' tab", @@ -54,26 +56,25 @@ "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" + "1": "It's a virtual computer, where all your services live." } }, "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 {}" } } }, diff --git a/assets/translations/ru.json b/assets/translations/ru.json index 0a7bbd2e6a..f6e17b7021 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -22,7 +22,9 @@ "nickname": "Никнейм", "loading": "Загрузка", "later": "Настрою потом", - "reset": "Reset" + "reset": "Reset", + "details": "Детальная информация", + "no_data": "Нет данных" }, "more": { "_comment": "вкладка еще", @@ -54,26 +56,25 @@ "page_title": "Ваш Дата-центр", "server": { "card_title": "Сервер", + "status": "Статус — в норме", "bottom_sheet": { - "1": "Это виртульный компьютер на котором работают все ваши сервисы.", - "2": "1 CPU, RAM 4Gb, 40Gb — $5 в месяц", - "3": "Статус — в норме" + "1": "Это виртульный компьютер на котором работают все ваши сервисы." } }, "domain": { "card_title": "Домен", + "status": "Статус — в норме", "bottom_sheet": { "1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы.", - "2": "{} — продлен до {}", - "3": "Статус — в норме" + "2": "{} — продлен до {}" } }, "backup": { "card_title": "Резервное копирование", + "status": "Статус — в норме", "bottom_sheet": { "1": "Выручит в любой ситуации: хакерская атака, удаление сервера и т.п.", - "2": "3Gb — бестплатно до 10Gb, последний вчера в {}", - "3": "Статус — в норме" + "2": "3Gb — бестплатно до 10Gb, последний вчера в {}" } } }, diff --git a/build.yaml b/build.yaml new file mode 100644 index 0000000000..709b623e77 --- /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/text_themes.dart b/lib/config/text_themes.dart index b64ab1a2f8..cf3831228b 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, diff --git a/lib/logic/api_maps/api_map.dart b/lib/logic/api_maps/api_map.dart index ce5484a599..5f4330d46c 100644 --- a/lib/logic/api_maps/api_map.dart +++ b/lib/logic/api_maps/api_map.dart @@ -10,7 +10,6 @@ import 'package:selfprivacy/logic/get_it/console.dart'; import 'package:selfprivacy/logic/models/message.dart'; abstract class ApiMap { - Future getClient() async { var dio = Dio(await options); if (hasLoger) { @@ -31,28 +30,15 @@ abstract class ApiMap { abstract final String rootAddress; abstract final bool hasLoger; abstract final bool isWithToken; + + ValidateStatus? validateStatus; + + void close(Dio client) { + client.close(); + validateStatus = null; + } } -// abstract class ApiMapOld { -// ApiMapOld() { -// var client = Dio()..interceptors.add(ConsoleInterceptor()); -// (client.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = -// (HttpClient client) { -// client.badCertificateCallback = -// (X509Certificate cert, String host, int port) => true; -// return client; -// }; -// loggedClient = client; -// } -// String? rootAddress; - -// late Dio loggedClient; - -// void close() { -// loggedClient.close(); -// } -// } - class ConsoleInterceptor extends InterceptorsWrapper { void addMessage(Message message) { getIt.get().addMessage(message); diff --git a/lib/logic/api_maps/backblaze.dart b/lib/logic/api_maps/backblaze.dart index f398975145..2a9888cded 100644 --- a/lib/logic/api_maps/backblaze.dart +++ b/lib/logic/api_maps/backblaze.dart @@ -11,7 +11,6 @@ class BackblazeApi extends ApiMap { if (isWithToken) { var backblazeCredential = getIt().backblazeCredential; var token = backblazeCredential!.applicationKey; - assert(token != null); options.headers = {'Authorization': 'Basic $token'}; } @@ -22,7 +21,6 @@ class BackblazeApi extends ApiMap { return options; } - ValidateStatus? validateStatus; @override String rootAddress = 'https://api.backblazeb2.com/b2api/v2/'; @@ -32,7 +30,7 @@ class BackblazeApi extends ApiMap { 'b2_authorize_account', options: Options(headers: {'Authorization': 'Basic $encodedApiKey'}), ); - client.close(); + close(client); if (response.statusCode == HttpStatus.ok) { return true; } else if (response.statusCode == HttpStatus.unauthorized) { diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index 40cbef4253..7dc9994d19 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -22,8 +22,6 @@ class CloudflareApi extends ApiMap { return options; } - ValidateStatus? validateStatus; - @override String rootAddress = 'https://api.cloudflare.com/client/v4'; @@ -36,8 +34,7 @@ class CloudflareApi extends ApiMap { Response response = await client.get('/user/tokens/verify', options: Options(headers: {'Authorization': 'Bearer $token'})); - client.close(); - validateStatus = null; + close(client); if (response.statusCode == HttpStatus.ok) { return true; @@ -48,7 +45,7 @@ class CloudflareApi extends ApiMap { } } - Future getZoneId(String domain) async { + Future getZoneId(String domain) async { validateStatus = (status) { return status == HttpStatus.ok || status == HttpStatus.forbidden; }; @@ -58,14 +55,9 @@ class CloudflareApi extends ApiMap { queryParameters: {'name': domain}, ); - client.close(); - validateStatus = null; + close(client); - try { - return response.data['result'][0]['id']; - } catch (error) { - return null; - } + return response.data['result'][0]['id']; } Future removeSimilarRecords({ @@ -92,7 +84,7 @@ class CloudflareApi extends ApiMap { } await Future.wait(allDeleteFutures); - client.close(); + close(client); } Future createMultipleDnsRecords({ @@ -118,7 +110,7 @@ class CloudflareApi extends ApiMap { } await Future.wait(allCreateFutures); - client.close(); + close(client); } List projectDnsRecords(String? domainName, String? ip4) { @@ -171,7 +163,7 @@ class CloudflareApi extends ApiMap { queryParameters: {'per_page': 50}, ); - client.close(); + close(client); return response.data['result'] .map((el) => el['name'] as String) .toList(); diff --git a/lib/logic/api_maps/hetzner.dart b/lib/logic/api_maps/hetzner.dart index 26018a51d3..a9cad08623 100644 --- a/lib/logic/api_maps/hetzner.dart +++ b/lib/logic/api_maps/hetzner.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/api_map.dart'; +import 'package:selfprivacy/logic/models/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'; @@ -29,8 +30,6 @@ class HetznerApi extends ApiMap { return options; } - ValidateStatus? validateStatus; - @override String rootAddress = 'https://api.hetzner.cloud/v1'; @@ -45,7 +44,7 @@ class HetznerApi extends ApiMap { headers: {'Authorization': 'Bearer $token'}, ), ); - client.close(); + close(client); if (response.statusCode == HttpStatus.ok) { return true; @@ -87,7 +86,7 @@ class HetznerApi extends ApiMap { List list = response.data['servers']; var server = list.firstWhere((el) => el['name'] == 'selfprivacy-server'); await client.delete('/servers/${server['id']}'); - client.close(); + close(client); } Future startServer({ @@ -96,7 +95,7 @@ class HetznerApi extends ApiMap { var client = await getClient(); await client.post('/servers/${server.id}/actions/poweron'); - client.close(); + close(client); return server.copyWith( startTime: DateTime.now(), @@ -108,7 +107,7 @@ class HetznerApi extends ApiMap { }) async { var client = await getClient(); await client.post('/servers/${server.id}/actions/poweron'); - client.close(); + close(client); return server.copyWith( startTime: DateTime.now(), ); @@ -118,13 +117,15 @@ class HetznerApi extends ApiMap { var hetznerServer = getIt().hetznerServer; var client = await getClient(); await client.post('/servers/${hetznerServer!.id}/metrics'); - client.close(); + close(client); } - getInfo() async { + Future getInfo() async { var hetznerServer = getIt().hetznerServer; var client = await getClient(); - await client.post('/servers/${hetznerServer!.id}'); - client.close(); + 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 0da2704b5e..2089e633f1 100644 --- a/lib/logic/api_maps/server.dart +++ b/lib/logic/api_maps/server.dart @@ -18,7 +18,6 @@ class ServerApi extends ApiMap { if (isWithToken) { var cloudFlareDomain = getIt().cloudFlareDomain; var domainName = cloudFlareDomain!.domainName; - assert(domainName != null); options = BaseOptions(baseUrl: 'https://api.$domainName'); } @@ -37,7 +36,7 @@ class ServerApi extends ApiMap { } catch (e) { res = false; } - client.close(); + close(client); return res; } diff --git a/lib/logic/cubit/app_config/app_config_cubit.dart b/lib/logic/cubit/app_config/app_config_cubit.dart index 2fcc365244..15e5f1faee 100644 --- a/lib/logic/cubit/app_config/app_config_cubit.dart +++ b/lib/logic/cubit/app_config/app_config_cubit.dart @@ -257,7 +257,7 @@ class AppConfigCubit extends Cubit { emit(state.copyWith(isLoading: true)); await repository.createServer( state.rootUser!, - state.cloudFlareDomain!.domainName!, + state.cloudFlareDomain!.domainName, state.cloudFlareKey!, onCancel: onCancel, onSuccess: onSuccess, diff --git a/lib/logic/cubit/app_config/app_config_state.dart b/lib/logic/cubit/app_config/app_config_state.dart index 6b9f8ee367..5a89ed042d 100644 --- a/lib/logic/cubit/app_config/app_config_state.dart +++ b/lib/logic/cubit/app_config/app_config_state.dart @@ -92,7 +92,6 @@ class AppConfigState extends Equatable { isServerReseted, hasFinalChecked, ]; - print('progress: $res'); return res; } } 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 0000000000..3d46142d70 --- /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 0000000000..bcd3462548 --- /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 0000000000..cf017658f8 --- /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/models/backblaze_credential.dart b/lib/logic/models/backblaze_credential.dart index 3496f4ced7..b9c0636456 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 305b33863d..c6ad373e69 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 d66e9a043d..9d85bfb1f7 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 d96cf9bc06..dcd95317a6 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 0000000000..98af1c3e2b --- /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 0000000000..40055d1955 --- /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_info.dart b/lib/logic/models/server_info.dart deleted file mode 100644 index 12c7e0a8df..0000000000 --- a/lib/logic/models/server_info.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -@JsonSerializable(createFactory: false) -class ServerInfo { - final String id; - final String name; - final ServerStatus status; - final DateTime created; - - ServerInfo(this.id, this.name, this.status, this.created); -} - -enum ServerStatus { - running, - initializing, - starting, - stopping, - off, - deleting, - migrating, - rebuilding, - unknown, -} diff --git a/lib/main.dart b/lib/main.dart index 141c95c2e9..231a7dca28 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -37,8 +37,6 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { AppSettingsState appSettings = context.watch().state; - var a = DateTime.parse('2021-03-23T20:00:06+00:00'); - print(a); return AnnotatedRegion( value: SystemUiOverlayStyle.light, // Manually changnig appbar color child: MaterialApp( diff --git a/lib/ui/components/brand_button/brand_button.dart b/lib/ui/components/brand_button/brand_button.dart index df6cfc3979..a2de859262 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, diff --git a/lib/ui/components/brand_text/brand_text.dart b/lib/ui/components/brand_text/brand_text.dart index 9dda8549df..1acbb4e8ba 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/one_page/one_page.dart b/lib/ui/components/one_page/one_page.dart new file mode 100644 index 0000000000..7cb27a83dc --- /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 0000000000..9a5e8baf57 --- /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/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index 89f4865306..ea6cf8c2a2 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -10,9 +10,15 @@ 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/components/one_page/one_page.dart'; import 'package:selfprivacy/ui/pages/providers/settings/settings.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); @@ -64,9 +70,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 = @@ -75,30 +83,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, @@ -139,20 +171,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 = [ @@ -161,7 +184,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: @@ -185,38 +208,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( @@ -242,5 +234,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 0000000000..e6db48fae9 --- /dev/null +++ b/lib/ui/pages/server_details/server_details.dart @@ -0,0 +1,290 @@ +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('General information')), + 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('Location')), + 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), + ], + ), + ], + ), + // BrandText.body1('providers.server.bottom_sheet.2'.tr()), + // SizedBox(height: 10), + // BrandText.body1('providers.server.bottom_sheet.3'.tr()), + ], + ), + ), + ], + ); + } 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 0000000000..b5d48e08a3 --- /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 69dea2b7d4..2407e14ac9 100644 --- a/lib/ui/pages/services/services.dart +++ b/lib/ui/pages/services/services.dart @@ -4,6 +4,7 @@ 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'; @@ -13,6 +14,7 @@ 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'; @@ -105,10 +107,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, @@ -170,9 +172,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, @@ -181,7 +181,6 @@ class _ServiceDetails extends StatelessWidget { : BrandColors.black, fontWeight: FontWeight.bold, decoration: TextDecoration.underline, - // height: 1.1, ); var textStyle = body1Style.copyWith( @@ -198,9 +197,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(), @@ -236,9 +236,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( @@ -259,9 +260,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( @@ -282,9 +284,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( @@ -306,9 +309,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://social.$domainName'), child: Text( @@ -329,9 +333,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( @@ -345,36 +350,44 @@ 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), - SizedBox(height: 10), - child, - ], - ), - ) - ], - ), - ); - }, + ), + ], + ), + ) + ], + ), + ), ), ); } @@ -397,3 +410,252 @@ class _ServiceDetails extends StatelessWidget { } } } + + +// class _ServiceDetails extends StatelessWidget { +// const _ServiceDetails({ +// Key? key, +// required this.serviceType, +// required this.icon, +// required this.status, +// required this.title, +// required this.changeTab, +// }) : super(key: key); + +// final ServiceTypes serviceType; +// final IconData icon; +// 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 53636e74e7..62eb624f6f 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 99f4331557..3c788bec14 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 8acb401620..271182e6c8 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/route_transitions/slide_bottom.dart b/lib/utils/route_transitions/slide_bottom.dart index 607ab105e8..380b11420a 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 0000000000..b0b438516e --- /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 83d5ac4b92..17c0befd4d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -357,6 +357,13 @@ 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.0" http: dependency: transitive description: @@ -789,6 +796,13 @@ 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: diff --git a/pubspec.yaml b/pubspec.yaml index d692fbcf46..ba5d042cb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: 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 @@ -36,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: