diff --git a/assets/translations/en.json b/assets/translations/en.json index 47d778fd..1c08293e 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -91,6 +91,28 @@ "status": "Status — Good", "bottom_sheet": { "1": "It's your personal internet address that will point to the server and other services of yours." + }, + "screen_title": "Domain and DNS", + "states": { + "ok": "Records are OK", + "error": "Problems found", + "error_subtitle": "Tap here to fix them", + "refreshing": "Refreshing status...", + "uninitialized": "Data is not retrieved yet" + }, + "record_description": { + "root": "Root domain", + "api": "SelfPrivacy API", + "cloud": "File cloud", + "git": "Git server", + "meet": "Video conference", + "social": "Social network", + "password": "Password manager", + "vpn": "VPN", + "mx": "MX record", + "dmarc": "DMARC record", + "spf": "SPF record", + "dkim": "DKIM key" } }, "backup": { @@ -120,7 +142,8 @@ "_comment": "Card shown when user skips initial setup", "1": "Please finish application setup using ", "2": "@:more.configuration_wizard", - "3": " for further work" + "3": " for further work", + "in_menu": "Server is not set up yet. Please finish setup using setup wizard for further work." }, "services": { "_comment": "Вкладка сервисы", diff --git a/assets/translations/ru.json b/assets/translations/ru.json index bd9c5ef9..288af050 100644 --- a/assets/translations/ru.json +++ b/assets/translations/ru.json @@ -91,6 +91,38 @@ "status": "Статус — в норме", "bottom_sheet": { "1": "Это ваш личный адрес в интернете, который будет указывать на сервер и другие ваши сервисы." + }, + "screen_title": "Домен и DNS", + "states": { + "ok": "Записи в норме", + "error": "Обнаружены проблемы", + "error_subtitle": "Нажмите здесь, чтобы исправить", + "refreshing": "Обновление данных...", + "uninitialized": "Данные ещё не получены" + }, + "record_description": { + "root": "Корневой домен", + "api": "SelfPrivacy API", + "cloud": "Файловое облако", + "git": "Git сервер", + "meet": "Видеоконференции", + "social": "Социальная сеть", + "password": "Менеджер паролей", + "vpn": "VPN", + "mx": "MX запись", + "dmarc": "DMARC запись", + "spf": "SPF запись", + "dkim": "DKIM ключ" + }, + "cards": { + "services": { + "title": "Сервисы", + "subtitle": "Записи типа “A” необходимые для работы сервисов." + }, + "email": { + "title": "Электронная почта", + "subtitle": "Записи необходимые для безопасного обмена электронной почтой." + } } }, "backup": { @@ -98,7 +130,7 @@ "status": "Статус — в норме", "bottom_sheet": { "1": "Выручит Вас в любой ситуации: хакерская атака, удаление сервера и т.д.", - "2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}." + "2": "Использовано 3Gb из бесплатых 10Gb. Последнее копирование была сделано вчера в {}." }, "reuploadKey": "Принудительно обновить ключ", "reuploadedKey": "Ключ на сервере обновлён", @@ -121,7 +153,8 @@ "_comment": "Карточка показывающая когда человек скипнул настройку, на карте текст из 3 блоков, средний содержит ссыку на мастер подключения", "1": "Завершите настройку приложения используя ", "2": "@:more.configuration_wizard", - "3": " для продолжения работы" + "3": " для продолжения работы", + "in_menu": "Сервер ещё не настроен, воспользуйтесь мастером подключения." }, "services": { "_comment": "Вкладка сервисы", diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 19f1eb67..8f9836bd 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/app_settings/app_settings_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; +import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; import 'package:selfprivacy/logic/cubit/services/services_cubit.dart'; @@ -20,6 +21,7 @@ class BlocAndProviderConfig extends StatelessWidget { var appConfigCubit = AppConfigCubit()..load(); var servicesCubit = ServicesCubit(appConfigCubit); var backupsCubit = BackupsCubit(appConfigCubit); + var dnsRecordsCubit = DnsRecordsCubit(appConfigCubit); return MultiProvider( providers: [ BlocProvider( @@ -33,6 +35,7 @@ class BlocAndProviderConfig extends StatelessWidget { BlocProvider(create: (_) => usersCubit..load(), lazy: false), BlocProvider(create: (_) => servicesCubit..load(), lazy: false), BlocProvider(create: (_) => backupsCubit..load(), lazy: false), + BlocProvider(create: (_) => dnsRecordsCubit..load()), BlocProvider( create: (_) => JobsCubit(usersCubit: usersCubit, servicesCubit: servicesCubit), diff --git a/lib/logic/api_maps/cloudflare.dart b/lib/logic/api_maps/cloudflare.dart index c9047a7b..b14bdf74 100644 --- a/lib/logic/api_maps/cloudflare.dart +++ b/lib/logic/api_maps/cloudflare.dart @@ -1,4 +1,5 @@ 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'; @@ -87,6 +88,36 @@ class CloudflareApi extends ApiMap { close(client); } + Future> getDnsRecords({ + required CloudFlareDomain cloudFlareDomain, + }) async { + var domainName = cloudFlareDomain.domainName; + var domainZoneId = cloudFlareDomain.zoneId; + + var url = '/zones/$domainZoneId/dns_records'; + + var client = await getClient(); + Response response = await client.get(url); + + List records = response.data['result'] ?? []; + var allRecords = []; + + for (var record in records) { + if (record['zone_name'] == domainName) { + allRecords.add(DnsRecord( + name: record['name'], + type: record['type'], + content: record['content'], + ttl: record['ttl'], + proxied: record['proxied'], + )); + } + } + + close(client); + return allRecords; + } + Future createMultipleDnsRecords({ String? ip4, required CloudFlareDomain cloudFlareDomain, @@ -113,33 +144,33 @@ class CloudflareApi extends ApiMap { close(client); } - List projectDnsRecords(String? domainName, String? ip4) { - var domainA = DnsRecords(type: 'A', name: domainName, content: ip4); + List projectDnsRecords(String? domainName, String? ip4) { + var domainA = DnsRecord(type: 'A', name: domainName, content: ip4); - var mx = DnsRecords(type: 'MX', name: '@', content: domainName); - var apiA = DnsRecords(type: 'A', name: 'api', content: ip4); - var cloudA = DnsRecords(type: 'A', name: 'cloud', content: ip4); - var gitA = DnsRecords(type: 'A', name: 'git', content: ip4); - var meetA = DnsRecords(type: 'A', name: 'meet', content: ip4); - var passwordA = DnsRecords(type: 'A', name: 'password', content: ip4); - var socialA = DnsRecords(type: 'A', name: 'social', content: ip4); - var vpn = DnsRecords(type: 'A', name: 'vpn', content: ip4); + var mx = DnsRecord(type: 'MX', name: '@', content: domainName); + var apiA = DnsRecord(type: 'A', name: 'api', content: ip4); + var cloudA = DnsRecord(type: 'A', name: 'cloud', content: ip4); + var gitA = DnsRecord(type: 'A', name: 'git', content: ip4); + var meetA = DnsRecord(type: 'A', name: 'meet', content: ip4); + var passwordA = DnsRecord(type: 'A', name: 'password', content: ip4); + var socialA = DnsRecord(type: 'A', name: 'social', content: ip4); + var vpn = DnsRecord(type: 'A', name: 'vpn', content: ip4); - var txt1 = DnsRecords( + var txt1 = DnsRecord( type: 'TXT', name: '_dmarc', content: 'v=DMARC1; p=none', ttl: 18000, ); - var txt2 = DnsRecords( + var txt2 = DnsRecord( type: 'TXT', name: domainName, content: 'v=spf1 a mx ip4:$ip4 -all', ttl: 18000, ); - return [ + return [ domainA, apiA, cloudA, @@ -159,7 +190,7 @@ class CloudflareApi extends ApiMap { final domainZoneId = cloudFlareDomain.zoneId; final url = '$rootAddress/zones/$domainZoneId/dns_records'; - final dkimRecord = DnsRecords( + final dkimRecord = DnsRecord( type: 'TXT', name: 'selector._domainkey', content: dkimRecordString, diff --git a/lib/logic/cubit/dns_records/dns_records_cubit.dart b/lib/logic/cubit/dns_records/dns_records_cubit.dart new file mode 100644 index 00000000..f35a9125 --- /dev/null +++ b/lib/logic/cubit/dns_records/dns_records_cubit.dart @@ -0,0 +1,184 @@ +import 'package:cubit_form/cubit_form.dart'; +import 'package:selfprivacy/logic/cubit/app_config_dependent/authentication_dependend_cubit.dart'; +import 'package:selfprivacy/logic/models/cloudflare_domain.dart'; +import 'package:selfprivacy/logic/models/dns_records.dart'; + +import '../../api_maps/cloudflare.dart'; +import '../../api_maps/server.dart'; + +part 'dns_records_state.dart'; + +class DnsRecordsCubit extends AppConfigDependendCubit { + DnsRecordsCubit(AppConfigCubit appConfigCubit) + : super(appConfigCubit, + DnsRecordsState(dnsState: DnsRecordsStatus.refreshing)); + + final api = ServerApi(); + final cloudflare = CloudflareApi(); + + Future load() async { + emit(DnsRecordsState( + dnsState: DnsRecordsStatus.refreshing, + dnsRecords: _getDesiredDnsRecords( + appConfigCubit.state.cloudFlareDomain?.domainName, "", ""))); + print('Loading DNS status'); + if (appConfigCubit.state is AppConfigFinished) { + final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; + final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + if (domain != null && ipAddress != null) { + final List records = + await cloudflare.getDnsRecords(cloudFlareDomain: domain); + final dkimPublicKey = await api.getDkim(); + final desiredRecords = + _getDesiredDnsRecords(domain.domainName, ipAddress, dkimPublicKey); + List foundRecords = []; + for (final record in desiredRecords) { + if (record.description == + 'providers.domain.record_description.dkim') { + final foundRecord = records.firstWhere( + (r) => r.name == record.name && r.type == record.type, + orElse: () => DnsRecord( + name: record.name, + type: record.type, + content: '', + ttl: 800, + proxied: false)); + // remove all spaces and tabulators from + // the foundRecord.content and the record.content + // to compare them + final foundContent = + foundRecord.content?.replaceAll(RegExp(r'\s+'), ''); + final content = record.content.replaceAll(RegExp(r'\s+'), ''); + if (foundContent == content) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } else { + if (records.any((r) => + r.name == record.name && + r.type == record.type && + r.content == record.content)) { + foundRecords.add(record.copyWith(isSatisfied: true)); + } else { + foundRecords.add(record.copyWith(isSatisfied: false)); + } + } + } + emit(DnsRecordsState( + dnsRecords: foundRecords, + dnsState: foundRecords.any((r) => r.isSatisfied == false) + ? DnsRecordsStatus.error + : DnsRecordsStatus.good, + )); + } else { + emit(DnsRecordsState()); + } + } + } + + @override + void onChange(Change change) { + print(change); + super.onChange(change); + } + + @override + Future clear() async { + emit(DnsRecordsState(dnsState: DnsRecordsStatus.error)); + } + + Future refresh() async { + emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); + await load(); + } + + Future fix() async { + emit(state.copyWith(dnsState: DnsRecordsStatus.refreshing)); + final CloudFlareDomain? domain = appConfigCubit.state.cloudFlareDomain; + final String? ipAddress = appConfigCubit.state.hetznerServer?.ip4; + final dkimPublicKey = await api.getDkim(); + await cloudflare.removeSimilarRecords(cloudFlareDomain: domain!); + await cloudflare.createMultipleDnsRecords( + cloudFlareDomain: domain, ip4: ipAddress); + await cloudflare.setDkim(dkimPublicKey, domain); + await load(); + } + + List _getDesiredDnsRecords( + String? domainName, String? ipAddress, String? dkimPublicKey) { + if (domainName == null || ipAddress == null || dkimPublicKey == null) { + return []; + } + return [ + DesiredDnsRecord( + name: domainName, + content: ipAddress, + description: 'providers.domain.record_description.root', + ), + DesiredDnsRecord( + name: 'api.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.api', + ), + DesiredDnsRecord( + name: 'cloud.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.cloud', + ), + DesiredDnsRecord( + name: 'git.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.git', + ), + DesiredDnsRecord( + name: 'meet.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.meet', + ), + DesiredDnsRecord( + name: 'social.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.social', + ), + DesiredDnsRecord( + name: 'password.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.password', + ), + DesiredDnsRecord( + name: 'vpn.$domainName', + content: ipAddress, + description: 'providers.domain.record_description.vpn', + ), + DesiredDnsRecord( + name: domainName, + content: domainName, + description: 'providers.domain.record_description.mx', + type: 'MX', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: '_dmarc.$domainName', + content: 'v=DMARC1; p=none', + description: 'providers.domain.record_description.dmarc', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: domainName, + content: 'v=spf1 a mx ip4:$ipAddress -all', + description: 'providers.domain.record_description.spf', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + DesiredDnsRecord( + name: 'selector._domainkey.$domainName', + content: dkimPublicKey, + description: 'providers.domain.record_description.dkim', + type: 'TXT', + category: DnsRecordsCategory.email, + ), + ]; + } +} diff --git a/lib/logic/cubit/dns_records/dns_records_state.dart b/lib/logic/cubit/dns_records/dns_records_state.dart new file mode 100644 index 00000000..dc594e74 --- /dev/null +++ b/lib/logic/cubit/dns_records/dns_records_state.dart @@ -0,0 +1,76 @@ +part of 'dns_records_cubit.dart'; + +enum DnsRecordsStatus { + uninitialized, + refreshing, + good, + error, +} + +enum DnsRecordsCategory { + services, + email, + other, +} + +class DnsRecordsState extends AppConfigDependendState { + const DnsRecordsState({ + this.dnsState = DnsRecordsStatus.uninitialized, + this.dnsRecords = const [], + }); + + final DnsRecordsStatus dnsState; + final List dnsRecords; + + @override + List get props => [ + dnsState, + dnsRecords, + ]; + + DnsRecordsState copyWith({ + DnsRecordsStatus? dnsState, + List? dnsRecords, + }) { + return DnsRecordsState( + dnsState: dnsState ?? this.dnsState, + dnsRecords: dnsRecords ?? this.dnsRecords, + ); + } +} + +class DesiredDnsRecord { + const DesiredDnsRecord({ + required this.name, + this.type = "A", + required this.content, + this.description = '', + this.category = DnsRecordsCategory.services, + this.isSatisfied = false, + }); + + final String name; + final String type; + final String content; + final String description; + final DnsRecordsCategory category; + final bool isSatisfied; + + DesiredDnsRecord copyWith({ + String? name, + String? type, + String? content, + String? description, + DnsRecordsCategory? category, + bool? isSatisfied, + }) { + return DesiredDnsRecord( + name: name ?? this.name, + type: type ?? this.type, + content: content ?? this.content, + description: description ?? this.description, + category: category ?? this.category, + isSatisfied: isSatisfied ?? this.isSatisfied, + ); + } +} diff --git a/lib/logic/models/dns_records.dart b/lib/logic/models/dns_records.dart index 80f1335e..349f2c41 100644 --- a/lib/logic/models/dns_records.dart +++ b/lib/logic/models/dns_records.dart @@ -3,8 +3,8 @@ import 'package:json_annotation/json_annotation.dart'; part 'dns_records.g.dart'; @JsonSerializable(createToJson: true, createFactory: false) -class DnsRecords { - DnsRecords({ +class DnsRecord { + DnsRecord({ required this.type, required this.name, required this.content, diff --git a/lib/logic/models/dns_records.g.dart b/lib/logic/models/dns_records.g.dart index 315cb447..df5d1cd9 100644 --- a/lib/logic/models/dns_records.g.dart +++ b/lib/logic/models/dns_records.g.dart @@ -6,7 +6,7 @@ part of 'dns_records.dart'; // JsonSerializableGenerator // ************************************************************************** -Map _$DnsRecordsToJson(DnsRecords instance) => +Map _$DnsRecordsToJson(DnsRecord instance) => { 'type': instance.type, 'name': instance.name, diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart new file mode 100644 index 00000000..8973e963 --- /dev/null +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -0,0 +1,219 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/config/get_it_config.dart'; +import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; +import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; +import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; +import 'package:selfprivacy/ui/components/brand_hero_screen/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/components/brand_icons/brand_icons.dart'; + +class DnsDetailsPage extends StatefulWidget { + @override + _DnsDetailsPageState createState() => _DnsDetailsPageState(); +} + +class _DnsDetailsPageState extends State { + Widget _getStateCard(DnsRecordsStatus dnsState, Function fixCallback) { + var description = ''; + var subtitle = ''; + var icon = Icon( + Icons.check, + color: Colors.green, + ); + switch (dnsState) { + case DnsRecordsStatus.uninitialized: + description = 'providers.domain.states.uninitialized'.tr(); + icon = Icon( + Icons.refresh, + ); + break; + case DnsRecordsStatus.refreshing: + description = 'providers.domain.states.refreshing'.tr(); + icon = Icon( + Icons.refresh, + ); + break; + case DnsRecordsStatus.good: + description = 'providers.domain.states.ok'.tr(); + icon = Icon( + Icons.check, + color: Colors.green, + ); + break; + case DnsRecordsStatus.error: + description = 'providers.domain.states.error'.tr(); + subtitle = 'providers.domain.states.error_subtitle'.tr(); + icon = Icon( + Icons.error, + color: Colors.red, + ); + break; + } + return ListTile( + onTap: dnsState == DnsRecordsStatus.error ? () => fixCallback() : null, + title: Text( + description, + style: Theme.of(context).textTheme.headline6, + ), + subtitle: subtitle != '' ? Text(subtitle) : null, + leading: icon, + ); + } + + @override + Widget build(BuildContext context) { + var isReady = context.watch().state is AppConfigFinished; + final domain = getIt().cloudFlareDomain?.domainName ?? ''; + var dnsCubit = context.watch().state; + + print(dnsCubit.dnsState); + + if (!isReady) { + return BrandHeroScreen( + hasBackButton: true, + headerTitle: '', + heroIcon: BrandIcons.globe, + heroTitle: 'providers.domain.screen_title'.tr(), + children: [ + BrandCards.outlined( + child: ListTile( + title: Text( + 'not_ready_card.in_menu'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + ), + ), + ], + ); + } + + return BrandHeroScreen( + hasBackButton: true, + heroSubtitle: domain, + heroIcon: BrandIcons.globe, + heroTitle: 'providers.domain.screen_title'.tr(), + children: [ + BrandCards.outlined( + child: Column( + children: [ + _getStateCard(dnsCubit.dnsState, () { + context.read().fix(); + }), + ], + ), + ), + + SizedBox(height: 16.0), + // Outlined card with a list of A records and their + // status. + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'providers.domain.cards.services.title'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + subtitle: Text( + 'providers.domain.cards.services.subtitle'.tr(), + style: Theme.of(context).textTheme.caption, + ), + ), + ...dnsCubit.dnsRecords + .where( + (dnsRecord) => + dnsRecord.category == DnsRecordsCategory.services, + ) + .map( + (dnsRecord) => Column( + children: [ + Divider( + height: 1.0, + ), + ListTile( + leading: Icon( + dnsRecord.isSatisfied + ? Icons.check + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Icons.refresh + : Icons.error, + color: dnsRecord.isSatisfied + ? Colors.green + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Colors.grey + : Colors.red, + ), + title: Text( + dnsRecord.description.tr(), + style: Theme.of(context).textTheme.labelLarge, + ), + subtitle: Text( + dnsRecord.name, + style: Theme.of(context).textTheme.caption, + ), + ), + ], + ), + ) + .toList(), + ], + ), + ), + SizedBox(height: 16.0), + BrandCards.outlined( + child: Column( + children: [ + ListTile( + title: Text( + 'providers.domain.cards.email.title'.tr(), + style: Theme.of(context).textTheme.headline6, + ), + subtitle: Text( + 'providers.domain.cards.email.subtitle'.tr(), + style: Theme.of(context).textTheme.caption, + ), + ), + ...dnsCubit.dnsRecords + .where( + (dnsRecord) => + dnsRecord.category == DnsRecordsCategory.email, + ) + .map( + (dnsRecord) => Column( + children: [ + Divider( + height: 1.0, + ), + ListTile( + leading: Icon( + dnsRecord.isSatisfied + ? Icons.check + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Icons.refresh + : Icons.error, + color: dnsRecord.isSatisfied + ? Colors.green + : dnsCubit.dnsState == + DnsRecordsStatus.refreshing + ? Colors.grey + : Colors.red, + ), + title: Text( + dnsRecord.description.tr(), + style: Theme.of(context).textTheme.labelLarge, + ), + ), + ], + ), + ) + .toList(), + ], + ), + ), + ], + ); + } +} diff --git a/lib/ui/pages/providers/providers.dart b/lib/ui/pages/providers/providers.dart index c89569c4..dc53ed2f 100644 --- a/lib/ui/pages/providers/providers.dart +++ b/lib/ui/pages/providers/providers.dart @@ -3,10 +3,9 @@ import 'package:flutter/material.dart'; import 'package:selfprivacy/config/brand_theme.dart'; import 'package:selfprivacy/logic/cubit/app_config/app_config_cubit.dart'; import 'package:selfprivacy/logic/cubit/backups/backups_cubit.dart'; -import 'package:selfprivacy/logic/cubit/jobs/jobs_cubit.dart'; +import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/providers/providers_cubit.dart'; import 'package:selfprivacy/logic/models/provider.dart'; -import 'package:selfprivacy/logic/models/state_types.dart'; import 'package:selfprivacy/ui/components/brand_bottom_sheet/brand_bottom_sheet.dart'; import 'package:selfprivacy/ui/components/brand_cards/brand_cards.dart'; import 'package:selfprivacy/ui/components/brand_header/brand_header.dart'; @@ -15,9 +14,9 @@ import 'package:selfprivacy/ui/components/icon_status_mask/icon_status_mask.dart import 'package:selfprivacy/ui/components/not_ready_card/not_ready_card.dart'; import 'package:selfprivacy/ui/helpers/modals.dart'; import 'package:selfprivacy/ui/pages/backup_details/backup_details.dart'; +import 'package:selfprivacy/ui/pages/dns_details/dns_details.dart'; import 'package:selfprivacy/ui/pages/server_details/server_details.dart'; import 'package:selfprivacy/utils/route_transitions/basic.dart'; -import 'package:selfprivacy/utils/ui_helpers.dart'; var navigatorKey = GlobalKey(); @@ -33,6 +32,18 @@ class _ProvidersPageState extends State { Widget build(BuildContext context) { var isReady = context.watch().state is AppConfigFinished; var isBackupInitialized = context.watch().state.isInitialized; + var dnsStatus = context.watch().state.dnsState; + + StateType getDnsStatus() { + if (dnsStatus == DnsRecordsStatus.uninitialized || + dnsStatus == DnsRecordsStatus.refreshing) { + return StateType.uninitialized; + } + if (dnsStatus == DnsRecordsStatus.error) { + return StateType.warning; + } + return StateType.stable; + } final cards = ProviderType.values .map( @@ -43,7 +54,9 @@ class _ProvidersPageState extends State { state: isReady ? (type == ProviderType.backup && !isBackupInitialized ? StateType.uninitialized - : StateType.stable) + : (type == ProviderType.domain) + ? getDnsStatus() + : StateType.stable) : StateType.uninitialized, type: type, ), @@ -103,19 +116,13 @@ class _Card extends StatelessWidget { break; case ProviderType.domain: - title = 'providers.domain.card_title'.tr(); + title = 'providers.domain.screen_title'.tr(); message = domainName; stableText = 'providers.domain.status'.tr(); - onTap = () => showBrandBottomSheet( - context: context, - builder: (BuildContext context) { - return _ProviderDetails( - provider: provider, - statusText: stableText, - ); - }, - ); + onTap = () => Navigator.of(context).push(materialRoute( + DnsDetailsPage(), + )); break; case ProviderType.backup: title = 'providers.backup.card_title'.tr(); @@ -150,77 +157,3 @@ class _Card extends StatelessWidget { ); } } - -class _ProviderDetails extends StatelessWidget { - const _ProviderDetails({ - Key? key, - required this.provider, - required this.statusText, - }) : super(key: key); - - final ProviderModel provider; - final String? statusText; - - @override - Widget build(BuildContext context) { - late String title; - late List children; - - var config = context.watch().state; - - var domainName = UiHelpers.getDomainName(config); - - switch (provider.type) { - case ProviderType.server: - throw ('wrong type'); - case ProviderType.domain: - title = 'providers.domain.card_title'.tr(); - children = [ - BrandText.body1('providers.domain.bottom_sheet.1'.tr()), - SizedBox(height: 10), - BrandText.body1(domainName), - SizedBox(height: 10), - BrandText.body1('providers.domain.status'.tr()), - ]; - break; - case ProviderType.backup: - title = 'providers.backup.card_title'.tr(); - children = [ - BrandText.body1('providers.backup.bottom_sheet.1'.tr()), - SizedBox(height: 10), - BrandText.body1( - 'providers.backup.bottom_sheet.2'.tr(args: [domainName, 'Time'])), - SizedBox(height: 10), - BrandText.body1('providers.backup.status'.tr()), - ]; - break; - } - return BrandBottomSheet( - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 40), - Padding( - padding: paddingH15V0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IconStatusMask( - status: provider.state, - child: Icon(provider.icon, size: 40, color: Colors.white), - ), - SizedBox(height: 10), - BrandText.h1(title), - SizedBox(height: 10), - ...children, - SizedBox(height: 30), - ], - ), - ) - ], - ), - ), - ); - } -}