diff --git a/lib/ui/molecules/cards/dns_state_card.dart b/lib/ui/molecules/cards/dns_state_card.dart new file mode 100644 index 00000000..60376e53 --- /dev/null +++ b/lib/ui/molecules/cards/dns_state_card.dart @@ -0,0 +1,76 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; +import 'package:selfprivacy/ui/atoms/cards/filled_card.dart'; + +class DnsStateCard extends StatelessWidget { + const DnsStateCard({ + required this.dnsState, + required this.fixCallback, + super.key, + }); + + final DnsRecordsStatus dnsState; + final Function fixCallback; + + @override + Widget build(final BuildContext context) { + String description = ''; + String subtitle = ''; + Icon icon = const Icon( + Icons.check_circle_outline, + size: 24.0, + ); + bool isError = false; + switch (dnsState) { + case DnsRecordsStatus.uninitialized: + description = 'domain.uninitialized'.tr(); + icon = const Icon( + Icons.refresh, + size: 24.0, + ); + isError = false; + break; + case DnsRecordsStatus.refreshing: + description = 'domain.refreshing'.tr(); + icon = const Icon( + Icons.refresh, + size: 24.0, + ); + isError = false; + break; + case DnsRecordsStatus.good: + description = 'domain.ok'.tr(); + icon = const Icon( + Icons.check_circle_outline, + size: 24.0, + ); + isError = false; + break; + case DnsRecordsStatus.error: + description = 'domain.error'.tr(); + subtitle = 'domain.error_subtitle'.tr(); + icon = const Icon( + Icons.error_outline, + size: 24.0, + ); + isError = true; + break; + } + return FilledCard( + error: isError, + child: ListTile( + onTap: dnsState == DnsRecordsStatus.error ? () => fixCallback() : null, + leading: icon, + title: Text(description), + subtitle: subtitle != '' ? Text(subtitle) : null, + textColor: isError + ? Theme.of(context).colorScheme.onErrorContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + iconColor: isError + ? Theme.of(context).colorScheme.onErrorContainer + : Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } +} diff --git a/lib/ui/molecules/list_items/dns_record_item.dart b/lib/ui/molecules/list_items/dns_record_item.dart new file mode 100644 index 00000000..56f0aff5 --- /dev/null +++ b/lib/ui/molecules/list_items/dns_record_item.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; + +class DnsRecordItem extends StatelessWidget { + const DnsRecordItem({ + required this.dnsRecord, + required this.refreshing, + super.key, + }); + + final DesiredDnsRecord dnsRecord; + final bool refreshing; + @override + Widget build(final BuildContext context) { + final Color goodColor = Theme.of(context).colorScheme.onSurface; + final Color errorColor = Theme.of(context).colorScheme.error; + final Color neutralColor = Theme.of(context).colorScheme.onSurface; + + return ListTile( + leading: Icon( + dnsRecord.isSatisfied + ? Icons.check_circle_outline + : refreshing + ? Icons.refresh + : Icons.error_outline, + color: dnsRecord.isSatisfied + ? goodColor + : refreshing + ? neutralColor + : errorColor, + ), + title: Text(dnsRecord.displayName ?? dnsRecord.name), + subtitle: Text( + dnsRecord.content, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ); + } +} diff --git a/lib/ui/pages/dns_details/dns_details.dart b/lib/ui/pages/dns_details/dns_details.dart index c0a70fef..14b4db42 100644 --- a/lib/ui/pages/dns_details/dns_details.dart +++ b/lib/ui/pages/dns_details/dns_details.dart @@ -1,14 +1,19 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:selfprivacy/config/get_it_config.dart'; import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; import 'package:selfprivacy/logic/cubit/dns_records/dns_records_cubit.dart'; import 'package:selfprivacy/logic/cubit/server_installation/server_installation_cubit.dart'; import 'package:selfprivacy/logic/get_it/resources_model.dart'; -import 'package:selfprivacy/ui/atoms/cards/filled_card.dart'; import 'package:selfprivacy/ui/atoms/icons/brand_icons.dart'; +import 'package:selfprivacy/ui/atoms/list_tiles/section_headline.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/molecules/cards/dns_state_card.dart'; +import 'package:selfprivacy/ui/molecules/list_items/dns_record_item.dart'; +import 'package:selfprivacy/utils/fake_data.dart'; +import 'package:skeletonizer/skeletonizer.dart'; @RoutePage() class DnsDetailsPage extends StatefulWidget { @@ -19,69 +24,6 @@ class DnsDetailsPage extends StatefulWidget { } class _DnsDetailsPageState extends State { - Widget _getStateCard( - final DnsRecordsStatus dnsState, - final Function fixCallback, - ) { - String description = ''; - String subtitle = ''; - Icon icon = const Icon( - Icons.check_circle_outline, - size: 24.0, - ); - bool isError = false; - switch (dnsState) { - case DnsRecordsStatus.uninitialized: - description = 'domain.uninitialized'.tr(); - icon = const Icon( - Icons.refresh, - size: 24.0, - ); - isError = false; - break; - case DnsRecordsStatus.refreshing: - description = 'domain.refreshing'.tr(); - icon = const Icon( - Icons.refresh, - size: 24.0, - ); - isError = false; - break; - case DnsRecordsStatus.good: - description = 'domain.ok'.tr(); - icon = const Icon( - Icons.check_circle_outline, - size: 24.0, - ); - isError = false; - break; - case DnsRecordsStatus.error: - description = 'domain.error'.tr(); - subtitle = 'domain.error_subtitle'.tr(); - icon = const Icon( - Icons.error_outline, - size: 24.0, - ); - isError = true; - break; - } - return FilledCard( - error: isError, - child: ListTile( - onTap: dnsState == DnsRecordsStatus.error ? () => fixCallback() : null, - leading: icon, - title: Text(description), - subtitle: subtitle != '' ? Text(subtitle) : null, - textColor: isError - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.onSurfaceVariant, - iconColor: isError - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.onSurfaceVariant, - ), - ); - } - @override Widget build(final BuildContext context) { final bool isReady = context.watch().state @@ -89,6 +31,8 @@ class _DnsDetailsPageState extends State { final String domain = getIt().serverDomain?.domainName ?? ''; final DnsRecordsState dnsCubit = context.watch().state; + final List dnsRecords = + context.watch().state.dnsRecords; print(dnsCubit.dnsState); @@ -102,9 +46,35 @@ class _DnsDetailsPageState extends State { ); } - final Color goodColor = Theme.of(context).colorScheme.onSurface; - final Color errorColor = Theme.of(context).colorScheme.error; - final Color neutralColor = Theme.of(context).colorScheme.onSurface; + final recordsToShow = + dnsRecords.isEmpty ? FakeSelfPrivacyData.desiredDnsRecords : dnsRecords; + final refreshing = + dnsCubit.dnsState == DnsRecordsStatus.refreshing || dnsRecords.isEmpty; + + List recordsSection( + final String title, + final String subtitle, + final DnsRecordsCategory category, + ) => + [ + SectionHeadline( + title: title, + subtitle: subtitle, + ), + ...recordsToShow + .where( + (final dnsRecord) => dnsRecord.category == category, + ) + .map( + (final dnsRecord) => Skeletonizer( + enabled: refreshing, + child: DnsRecordItem( + dnsRecord: dnsRecord, + refreshing: refreshing, + ), + ), + ), + ]; return BrandHeroScreen( hasBackButton: true, @@ -112,132 +82,30 @@ class _DnsDetailsPageState extends State { heroIcon: BrandIcons.globe, heroTitle: 'domain.screen_title'.tr(), children: [ - _getStateCard( - dnsCubit.dnsState, - () { + DnsStateCard( + dnsState: dnsCubit.dnsState, + fixCallback: () { context.read().fix(); }, ), - const SizedBox(height: 16.0), - ListTile( - title: Text( - 'domain.services_title'.tr(), - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'domain.services_subtitle'.tr(), - style: Theme.of(context).textTheme.labelMedium, - ), + const Gap(8.0), + ...recordsSection( + 'domain.services_title'.tr(), + 'domain.services_subtitle'.tr(), + DnsRecordsCategory.services, ), - ...dnsCubit.dnsRecords - .where( - (final dnsRecord) => - dnsRecord.category == DnsRecordsCategory.services, - ) - .map( - (final dnsRecord) => Column( - children: [ - ListTile( - leading: Icon( - dnsRecord.isSatisfied - ? Icons.check_circle_outline - : dnsCubit.dnsState == DnsRecordsStatus.refreshing - ? Icons.refresh - : Icons.error_outline, - color: dnsRecord.isSatisfied - ? goodColor - : dnsCubit.dnsState == DnsRecordsStatus.refreshing - ? neutralColor - : errorColor, - ), - title: Text(dnsRecord.displayName ?? dnsRecord.name), - subtitle: Text(dnsRecord.content), - ), - ], - ), - ), - const SizedBox(height: 16.0), - ListTile( - title: Text( - 'domain.email_title'.tr(), - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'domain.email_subtitle'.tr(), - style: Theme.of(context).textTheme.labelMedium, - ), + const Gap(8.0), + ...recordsSection( + 'domain.email_title'.tr(), + 'domain.email_subtitle'.tr(), + DnsRecordsCategory.email, ), - ...dnsCubit.dnsRecords - .where( - (final dnsRecord) => - dnsRecord.category == DnsRecordsCategory.email, - ) - .map( - (final dnsRecord) => Column( - children: [ - ListTile( - leading: Icon( - dnsRecord.isSatisfied - ? Icons.check_circle_outline - : dnsCubit.dnsState == DnsRecordsStatus.refreshing - ? Icons.refresh - : Icons.error_outline, - color: dnsRecord.isSatisfied - ? goodColor - : dnsCubit.dnsState == DnsRecordsStatus.refreshing - ? neutralColor - : errorColor, - ), - title: Text(dnsRecord.displayName ?? dnsRecord.name), - subtitle: Text(dnsRecord.name), - ), - ], - ), - ), - const SizedBox(height: 16.0), - ListTile( - title: Text( - 'domain.other_title'.tr(), - style: Theme.of(context).textTheme.headlineSmall!.copyWith( - color: Theme.of(context).colorScheme.secondary, - ), - ), - subtitle: Text( - 'domain.other_subtitle'.tr(), - style: Theme.of(context).textTheme.labelMedium, - ), + const Gap(8.0), + ...recordsSection( + 'domain.other_title'.tr(), + 'domain.other_subtitle'.tr(), + DnsRecordsCategory.other, ), - ...dnsCubit.dnsRecords - .where( - (final dnsRecord) => - dnsRecord.category == DnsRecordsCategory.other, - ) - .map( - (final dnsRecord) => Column( - children: [ - ListTile( - leading: Icon( - dnsRecord.isSatisfied - ? Icons.check_circle_outline - : dnsCubit.dnsState == DnsRecordsStatus.refreshing - ? Icons.refresh - : Icons.error_outline, - color: dnsRecord.isSatisfied - ? goodColor - : dnsCubit.dnsState == DnsRecordsStatus.refreshing - ? neutralColor - : errorColor, - ), - title: Text(dnsRecord.displayName ?? dnsRecord.name), - subtitle: Text(dnsRecord.content), - ), - ], - ), - ), ], ); } diff --git a/lib/utils/fake_data.dart b/lib/utils/fake_data.dart new file mode 100644 index 00000000..a2fc4ea6 --- /dev/null +++ b/lib/utils/fake_data.dart @@ -0,0 +1,31 @@ +import 'package:selfprivacy/logic/api_maps/rest_maps/dns_providers/desired_dns_record.dart'; + +/// Fake data collections to fill skeletons +class FakeSelfPrivacyData { + static final List desiredDnsRecords = [ + ...List.generate( + 6, + (final index) => const DesiredDnsRecord( + type: 'A', + name: 'example.com', + description: 'Service name', + content: '192.0.2.100', + ), + ), + ...List.generate( + 4, + (final index) => const DesiredDnsRecord( + type: 'TXT', + name: 'example.com', + description: 'Service name', + content: 'Some text text text', + ), + ), + const DesiredDnsRecord( + type: 'CAA', + name: 'example.com', + description: 'Service name', + content: 'Some very very long text text text', + ), + ]; +} diff --git a/pubspec.lock b/pubspec.lock index 70062926..9e0a8a71 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1186,6 +1186,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae" + url: "https://pub.dev" + source: hosted + version: "1.4.2" sky_engine: dependency: transitive description: flutter @@ -1403,10 +1411,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 15b86878..22f5eb3b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: pretty_dio_logger: ^1.4.0 provider: ^6.1.2 pub_semver: ^2.1.4 + skeletonizer: ^1.4.2 timezone: ^0.9.4 url_launcher: ^6.3.0 # wakelock: ^0.6.2 # TODO: Developer is not available, update later.