From c61b3f5b1954ce3e2154d9260908126c458c6dce Mon Sep 17 00:00:00 2001 From: Inex Code Date: Thu, 12 Dec 2024 21:56:26 +0300 Subject: [PATCH] refactor(ui): Refactor charts wrapper --- .../atoms/chart_elements/colored_circle.dart | 24 ++ lib/ui/atoms/chart_elements/legend.dart | 26 ++ lib/ui/molecules/buttons/period_selector.dart | 42 +++ lib/ui/molecules/cards/chart_card.dart | 92 +++++ .../charts/cpu_chart.dart | 2 +- .../charts/disk_charts.dart | 2 +- .../charts/generic_chart.dart | 0 .../charts/memory_chart.dart | 2 +- .../charts/network_charts.dart | 2 +- lib/ui/pages/server_details/charts/chart.dart | 333 +++--------------- .../memory_usage_by_service_screen.dart | 29 +- .../server_details/server_details_screen.dart | 12 +- 12 files changed, 254 insertions(+), 312 deletions(-) create mode 100644 lib/ui/atoms/chart_elements/colored_circle.dart create mode 100644 lib/ui/atoms/chart_elements/legend.dart create mode 100644 lib/ui/molecules/buttons/period_selector.dart create mode 100644 lib/ui/molecules/cards/chart_card.dart rename lib/ui/{pages/server_details => molecules}/charts/cpu_chart.dart (93%) rename lib/ui/{pages/server_details => molecules}/charts/disk_charts.dart (95%) rename lib/ui/{pages/server_details => molecules}/charts/generic_chart.dart (100%) rename lib/ui/{pages/server_details => molecules}/charts/memory_chart.dart (93%) rename lib/ui/{pages/server_details => molecules}/charts/network_charts.dart (97%) diff --git a/lib/ui/atoms/chart_elements/colored_circle.dart b/lib/ui/atoms/chart_elements/colored_circle.dart new file mode 100644 index 00000000..393d9ec2 --- /dev/null +++ b/lib/ui/atoms/chart_elements/colored_circle.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class ColoredCircle extends StatelessWidget { + const ColoredCircle({ + required this.color, + super.key, + }); + + final Color color; + + @override + Widget build(final BuildContext context) => Container( + width: 10, + height: 10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: color.withOpacity(0.4), + border: Border.all( + color: color, + width: 1.5, + ), + ), + ); +} diff --git a/lib/ui/atoms/chart_elements/legend.dart b/lib/ui/atoms/chart_elements/legend.dart new file mode 100644 index 00000000..891535ff --- /dev/null +++ b/lib/ui/atoms/chart_elements/legend.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:selfprivacy/ui/atoms/chart_elements/colored_circle.dart'; + +class Legend extends StatelessWidget { + const Legend({ + required this.color, + required this.text, + super.key, + }); + + final String text; + final Color color; + @override + Widget build(final BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + ColoredCircle(color: color), + const SizedBox(width: 5), + Text( + text, + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ); +} diff --git a/lib/ui/molecules/buttons/period_selector.dart b/lib/ui/molecules/buttons/period_selector.dart new file mode 100644 index 00000000..7fe01ce3 --- /dev/null +++ b/lib/ui/molecules/buttons/period_selector.dart @@ -0,0 +1,42 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:selfprivacy/logic/common_enum/common_enum.dart'; +import 'package:selfprivacy/ui/atoms/buttons/segmented_buttons.dart'; + +class PeriodSelector extends StatelessWidget { + const PeriodSelector({ + required this.period, + required this.onChange, + super.key, + }); + + final Period period; + final Function(Period) onChange; + + @override + Widget build(final BuildContext context) => SegmentedButtons( + isSelected: [ + period == Period.month, + period == Period.day, + period == Period.hour, + ], + onPressed: (final index) { + switch (index) { + case 0: + onChange(Period.month); + break; + case 1: + onChange(Period.day); + break; + case 2: + onChange(Period.hour); + break; + } + }, + titles: [ + 'resource_chart.month'.tr(), + 'resource_chart.day'.tr(), + 'resource_chart.hour'.tr(), + ], + ); +} diff --git a/lib/ui/molecules/cards/chart_card.dart b/lib/ui/molecules/cards/chart_card.dart new file mode 100644 index 00000000..5966c0f2 --- /dev/null +++ b/lib/ui/molecules/cards/chart_card.dart @@ -0,0 +1,92 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:selfprivacy/ui/atoms/cards/filled_card.dart'; + +class ChartCard extends StatelessWidget { + const ChartCard({ + required this.title, + required this.chart, + required this.isLoading, + this.trailing = const [], + this.legendItems = const [], + super.key, + }); + + final String title; + final Widget? chart; + final bool isLoading; + final List trailing; + final List legendItems; + + @override + Widget build(final BuildContext context) => FilledCard( + clipped: false, + mergeSemantics: trailing.isEmpty, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExcludeSemantics( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + title, + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + Flexible( + fit: FlexFit.loose, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.end, + runAlignment: WrapAlignment.end, + children: legendItems, + ), + ), + ], + ), + ), + const Gap(16), + Stack( + alignment: Alignment.center, + children: [ + chart ?? const SizedBox.shrink(), + AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isLoading ? 1 : 0, + child: const _GraphLoadingCardContent(), + ), + ], + ), + if (trailing.isNotEmpty) const Divider(), + ...trailing, + ], + ), + ), + ); +} + +class _GraphLoadingCardContent extends StatelessWidget { + const _GraphLoadingCardContent(); + + @override + Widget build(final BuildContext context) => SizedBox( + height: 200, + child: Semantics( + label: 'resource_chart.loading'.tr(), + child: const Center(child: CircularProgressIndicator.adaptive()), + ), + ); +} diff --git a/lib/ui/pages/server_details/charts/cpu_chart.dart b/lib/ui/molecules/charts/cpu_chart.dart similarity index 93% rename from lib/ui/pages/server_details/charts/cpu_chart.dart rename to lib/ui/molecules/charts/cpu_chart.dart index bf4ca506..4a78995b 100644 --- a/lib/ui/pages/server_details/charts/cpu_chart.dart +++ b/lib/ui/molecules/charts/cpu_chart.dart @@ -1,6 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; +import 'package:selfprivacy/ui/molecules/charts/generic_chart.dart'; class CpuChart extends GenericLineChart { const CpuChart({ diff --git a/lib/ui/pages/server_details/charts/disk_charts.dart b/lib/ui/molecules/charts/disk_charts.dart similarity index 95% rename from lib/ui/pages/server_details/charts/disk_charts.dart rename to lib/ui/molecules/charts/disk_charts.dart index 359dc994..f6981af9 100644 --- a/lib/ui/pages/server_details/charts/disk_charts.dart +++ b/lib/ui/molecules/charts/disk_charts.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; +import 'package:selfprivacy/ui/molecules/charts/generic_chart.dart'; import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart'; class DiskChart extends GenericLineChart { diff --git a/lib/ui/pages/server_details/charts/generic_chart.dart b/lib/ui/molecules/charts/generic_chart.dart similarity index 100% rename from lib/ui/pages/server_details/charts/generic_chart.dart rename to lib/ui/molecules/charts/generic_chart.dart diff --git a/lib/ui/pages/server_details/charts/memory_chart.dart b/lib/ui/molecules/charts/memory_chart.dart similarity index 93% rename from lib/ui/pages/server_details/charts/memory_chart.dart rename to lib/ui/molecules/charts/memory_chart.dart index 828eb370..39444333 100644 --- a/lib/ui/pages/server_details/charts/memory_chart.dart +++ b/lib/ui/molecules/charts/memory_chart.dart @@ -1,6 +1,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; +import 'package:selfprivacy/ui/molecules/charts/generic_chart.dart'; class MemoryChart extends GenericLineChart { const MemoryChart({ diff --git a/lib/ui/pages/server_details/charts/network_charts.dart b/lib/ui/molecules/charts/network_charts.dart similarity index 97% rename from lib/ui/pages/server_details/charts/network_charts.dart rename to lib/ui/molecules/charts/network_charts.dart index 605cbfa6..beaf9898 100644 --- a/lib/ui/pages/server_details/charts/network_charts.dart +++ b/lib/ui/molecules/charts/network_charts.dart @@ -4,7 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/generic_chart.dart'; +import 'package:selfprivacy/ui/molecules/charts/generic_chart.dart'; class NetworkChart extends GenericLineChart { const NetworkChart({ diff --git a/lib/ui/pages/server_details/charts/chart.dart b/lib/ui/pages/server_details/charts/chart.dart index c20580d3..86a1e8b2 100644 --- a/lib/ui/pages/server_details/charts/chart.dart +++ b/lib/ui/pages/server_details/charts/chart.dart @@ -8,149 +8,55 @@ class _Chart extends StatelessWidget { final MetricsState state = cubit.state; List charts; + final metricsLoaded = state is MetricsLoaded; + if (state is MetricsLoaded || state is MetricsLoading) { charts = [ - FilledCard( - clipped: false, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'resource_chart.cpu_title'.tr(), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - Stack( - alignment: Alignment.center, - children: [ - if (state is MetricsLoaded) getCpuChart(state), - AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: state is MetricsLoading ? 1 : 0, - child: const _GraphLoadingCardContent(), - ), - ], - ), - ], - ), - ), + ChartCard( + title: 'resource_chart.cpu_title'.tr(), + chart: metricsLoaded ? getCpuChart(state) : null, + isLoading: !metricsLoaded, ), const SizedBox(height: 8), - if (!(state is MetricsLoaded && state.memoryMetrics == null)) - FilledCard( - clipped: false, - mergeSemantics: false, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'resource_chart.memory'.tr(), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - const SizedBox(height: 16), - Stack( - alignment: Alignment.center, - children: [ - if (state is MetricsLoaded && state.memoryMetrics != null) - getMemoryChart(state), - AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: state is MetricsLoading ? 1 : 0, - child: const _GraphLoadingCardContent(), - ), - ], - ), - const Divider(), - ListTile( - title: Text('resource_chart.view_usage_by_service'.tr()), - leading: Icon( - Icons.area_chart_outlined, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - onTap: () { - context.pushRoute( - const MemoryUsageByServiceRoute(), - ); - }, - enabled: state is MetricsLoaded, - ), - ], + if (!(metricsLoaded && state.memoryMetrics == null)) + ChartCard( + title: 'resource_chart.memory'.tr(), + chart: metricsLoaded ? getMemoryChart(state) : null, + isLoading: !metricsLoaded, + trailing: [ + ListTile( + title: Text('resource_chart.view_usage_by_service'.tr()), + leading: Icon( + Icons.area_chart_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + onTap: () { + context.pushRoute( + const MemoryUsageByServiceRoute(), + ); + }, + enabled: metricsLoaded, ), - ), + ], ), const SizedBox(height: 8), - FilledCard( - clipped: false, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ExcludeSemantics( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - 'resource_chart.network_title'.tr(), - style: - Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - ), - Flexible( - fit: FlexFit.loose, - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - alignment: WrapAlignment.end, - runAlignment: WrapAlignment.end, - children: [ - Legend( - color: Theme.of(context).colorScheme.primary, - text: 'resource_chart.in'.tr(), - ), - Legend( - color: Theme.of(context).colorScheme.tertiary, - text: 'resource_chart.out'.tr(), - ), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 20), - Stack( - alignment: Alignment.center, - children: [ - if (state is MetricsLoaded) getBandwidthChart(state), - AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: state is MetricsLoading ? 1 : 0, - child: const _GraphLoadingCardContent(), - ), - ], - ), - ], + ChartCard( + title: 'resource_chart.network_title'.tr(), + chart: metricsLoaded ? getBandwidthChart(state) : null, + isLoading: !metricsLoaded, + legendItems: [ + Legend( + color: Theme.of(context).colorScheme.primary, + text: 'resource_chart.in'.tr(), ), - ), + Legend( + color: Theme.of(context).colorScheme.tertiary, + text: 'resource_chart.out'.tr(), + ), + ], ), const SizedBox(height: 8), - if (!(state is MetricsLoaded && state.diskMetrics == null)) + if (!(metricsLoaded && state.diskMetrics == null)) Builder( builder: (final context) { List getDisksGraphData( @@ -184,69 +90,20 @@ class _Chart extends StatelessWidget { final disksGraphData = getDisksGraphData(context); - return FilledCard( - clipped: false, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ExcludeSemantics( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - 'resource_chart.disk_title'.tr(), - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, - ), - ), - ), - Flexible( - fit: FlexFit.loose, - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - alignment: WrapAlignment.end, - runAlignment: WrapAlignment.end, - children: disksGraphData - .map( - (final disk) => Legend( - color: disk.color, - text: disk.volume.displayName, - ), - ) - .toList(), - ), - ), - ], - ), + return ChartCard( + title: 'resource_chart.disk_title'.tr(), + chart: (metricsLoaded && state.diskMetrics != null) + ? getDiskChart(state, disksGraphData) + : null, + isLoading: !metricsLoaded, + legendItems: disksGraphData + .map( + (final disk) => Legend( + color: disk.color, + text: disk.volume.displayName, ), - const SizedBox(height: 20), - Stack( - alignment: Alignment.center, - children: [ - if (state is MetricsLoaded && - state.diskMetrics != null) - getDiskChart(state, disksGraphData), - AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: state is MetricsLoading ? 1 : 0, - child: const _GraphLoadingCardContent(), - ), - ], - ), - ], - ), - ), + ) + .toList(), ); }, ), @@ -278,30 +135,9 @@ class _Chart extends StatelessWidget { return Column( children: [ if (state is! MetricsUnsupported) - SegmentedButtons( - isSelected: [ - period == Period.month, - period == Period.day, - period == Period.hour, - ], - onPressed: (final index) { - switch (index) { - case 0: - cubit.changePeriod(Period.month); - break; - case 1: - cubit.changePeriod(Period.day); - break; - case 2: - cubit.changePeriod(Period.hour); - break; - } - }, - titles: [ - 'resource_chart.month'.tr(), - 'resource_chart.day'.tr(), - 'resource_chart.hour'.tr(), - ], + PeriodSelector( + period: period, + onChange: cubit.changePeriod, ), const SizedBox(height: 8), ...charts, @@ -374,65 +210,6 @@ Widget getDiskChart( ); } -class _GraphLoadingCardContent extends StatelessWidget { - const _GraphLoadingCardContent(); - - @override - Widget build(final BuildContext context) => SizedBox( - height: 200, - child: Semantics( - label: 'resource_chart.loading'.tr(), - child: const Center(child: CircularProgressIndicator.adaptive()), - ), - ); -} - -class Legend extends StatelessWidget { - const Legend({ - required this.color, - required this.text, - super.key, - }); - - final String text; - final Color color; - @override - Widget build(final BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - _ColoredBox(color: color), - const SizedBox(width: 5), - Text( - text, - style: Theme.of(context).textTheme.labelSmall, - ), - ], - ); -} - -class _ColoredBox extends StatelessWidget { - const _ColoredBox({ - required this.color, - }); - - final Color color; - - @override - Widget build(final BuildContext context) => Container( - width: 10, - height: 10, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: color.withOpacity(0.4), - border: Border.all( - color: color, - width: 1.5, - ), - ), - ); -} - class DiskGraphData { DiskGraphData({ required this.volume, diff --git a/lib/ui/pages/server_details/memory_usage_by_service_screen.dart b/lib/ui/pages/server_details/memory_usage_by_service_screen.dart index fff47af5..41452014 100644 --- a/lib/ui/pages/server_details/memory_usage_by_service_screen.dart +++ b/lib/ui/pages/server_details/memory_usage_by_service_screen.dart @@ -8,9 +8,9 @@ import 'package:selfprivacy/logic/bloc/services/services_bloc.dart'; import 'package:selfprivacy/logic/common_enum/common_enum.dart'; import 'package:selfprivacy/logic/cubit/metrics/metrics_cubit.dart'; import 'package:selfprivacy/logic/models/disk_size.dart'; -import 'package:selfprivacy/ui/atoms/buttons/segmented_buttons.dart'; import 'package:selfprivacy/ui/atoms/icons/brand_icons.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/molecules/buttons/period_selector.dart'; import 'package:selfprivacy/ui/molecules/placeholders/empty_page_placeholder.dart'; import 'package:selfprivacy/ui/router/router.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -104,30 +104,9 @@ class _MemoryUsageByServiceContents extends StatelessWidget { return BrandHeroScreen( heroTitle: 'resource_chart.memory'.tr(), children: [ - SegmentedButtons( - isSelected: [ - period == Period.month, - period == Period.day, - period == Period.hour, - ], - onPressed: (final index) { - switch (index) { - case 0: - cubit.changePeriod(Period.month); - break; - case 1: - cubit.changePeriod(Period.day); - break; - case 2: - cubit.changePeriod(Period.hour); - break; - } - }, - titles: [ - 'resource_chart.month'.tr(), - 'resource_chart.day'.tr(), - 'resource_chart.hour'.tr(), - ], + PeriodSelector( + period: period, + onChange: cubit.changePeriod, ), ...children, ], diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index eeced536..86801d8b 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -9,16 +9,18 @@ import 'package:selfprivacy/logic/cubit/server_installation/server_installation_ import 'package:selfprivacy/logic/models/disk_status.dart'; import 'package:selfprivacy/logic/models/metrics.dart'; import 'package:selfprivacy/theming/harmonized_basic_colors.dart'; -import 'package:selfprivacy/ui/atoms/buttons/segmented_buttons.dart'; import 'package:selfprivacy/ui/atoms/cards/filled_card.dart'; +import 'package:selfprivacy/ui/atoms/chart_elements/legend.dart'; import 'package:selfprivacy/ui/atoms/icons/brand_icons.dart'; import 'package:selfprivacy/ui/layouts/brand_hero_screen.dart'; +import 'package:selfprivacy/ui/molecules/buttons/period_selector.dart'; +import 'package:selfprivacy/ui/molecules/cards/chart_card.dart'; import 'package:selfprivacy/ui/molecules/cards/server_text_details_card.dart'; import 'package:selfprivacy/ui/molecules/cards/storage_card.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/cpu_chart.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/disk_charts.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/memory_chart.dart'; -import 'package:selfprivacy/ui/pages/server_details/charts/network_charts.dart'; +import 'package:selfprivacy/ui/molecules/charts/cpu_chart.dart'; +import 'package:selfprivacy/ui/molecules/charts/disk_charts.dart'; +import 'package:selfprivacy/ui/molecules/charts/memory_chart.dart'; +import 'package:selfprivacy/ui/molecules/charts/network_charts.dart'; import 'package:selfprivacy/ui/router/router.dart'; part 'charts/chart.dart';