From c193a4a0ca7ee2727a83c3b69a5146aa5f8f15e7 Mon Sep 17 00:00:00 2001 From: Nikolas Rimikis Date: Mon, 23 Oct 2023 11:40:23 +0200 Subject: [PATCH] feat(neon): add AdaptiveListTile Signed-off-by: Nikolas Rimikis --- .cspell/dart_flutter.txt | 1 + .../widgets/account_settings_tile.dart | 8 +- .../widgets/custom_settings_tile.dart | 11 +- .../neon/neon/lib/src/utils/adaptive.dart | 19 +++ .../src/widgets/account_switcher_button.dart | 3 +- .../neon/lib/src/widgets/account_tile.dart | 23 +-- .../widgets/adaptive_widgets/list_tile.dart | 135 ++++++++++++++++++ packages/neon/neon/lib/src/widgets/error.dart | 8 +- .../src/widgets/unified_search_results.dart | 5 +- .../neon/lib/src/widgets/validation_tile.dart | 3 +- 10 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 packages/neon/neon/lib/src/utils/adaptive.dart create mode 100644 packages/neon/neon/lib/src/widgets/adaptive_widgets/list_tile.dart diff --git a/.cspell/dart_flutter.txt b/.cspell/dart_flutter.txt index f1f2d865..15af3041 100644 --- a/.cspell/dart_flutter.txt +++ b/.cspell/dart_flutter.txt @@ -1,4 +1,5 @@ autofocus +cupertino endtemplate expando gapless diff --git a/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart b/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart index b444ab16..1c8ce8f4 100644 --- a/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart +++ b/packages/neon/neon/lib/src/settings/widgets/account_settings_tile.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; import 'package:neon/src/models/account.dart'; @@ -18,11 +20,11 @@ class AccountSettingsTile extends SettingsTile { /// {@macro neon.AccountTile.account} final Account account; - /// {@macro neon.AccountTile.trailing} + /// {@macro neon.AdaptiveListTile.trailing} final Widget? trailing; - /// {@macro neon.AccountTile.onTap} - final GestureTapCallback? onTap; + /// {@macro neon.AdaptiveListTile.onTap} + final FutureOr Function()? onTap; @override Widget build(final BuildContext context) => NeonAccountTile( diff --git a/packages/neon/neon/lib/src/settings/widgets/custom_settings_tile.dart b/packages/neon/neon/lib/src/settings/widgets/custom_settings_tile.dart index 6e216943..6161651b 100644 --- a/packages/neon/neon/lib/src/settings/widgets/custom_settings_tile.dart +++ b/packages/neon/neon/lib/src/settings/widgets/custom_settings_tile.dart @@ -1,11 +1,14 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; import 'package:neon/src/settings/widgets/settings_tile.dart'; +import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; @internal class CustomSettingsTile extends SettingsTile { const CustomSettingsTile({ - this.title, + required this.title, this.subtitle, this.leading, this.trailing, @@ -13,14 +16,14 @@ class CustomSettingsTile extends SettingsTile { super.key, }); - final Widget? title; + final Widget title; final Widget? subtitle; final Widget? leading; final Widget? trailing; - final GestureTapCallback? onTap; + final FutureOr Function()? onTap; @override - Widget build(final BuildContext context) => ListTile( + Widget build(final BuildContext context) => AdaptiveListTile( title: title, subtitle: subtitle, leading: leading, diff --git a/packages/neon/neon/lib/src/utils/adaptive.dart b/packages/neon/neon/lib/src/utils/adaptive.dart new file mode 100644 index 00000000..f2c3a4de --- /dev/null +++ b/packages/neon/neon/lib/src/utils/adaptive.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +/// Returns whether the current platform is a Cupertino one. +/// +/// This is true for both `TargetPlatform.iOS` and `TargetPlatform.macOS`. +bool isCupertino(final BuildContext context) { + final theme = Theme.of(context); + + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return false; + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return true; + } +} diff --git a/packages/neon/neon/lib/src/widgets/account_switcher_button.dart b/packages/neon/neon/lib/src/widgets/account_switcher_button.dart index 65b23a5f..d021aab8 100644 --- a/packages/neon/neon/lib/src/widgets/account_switcher_button.dart +++ b/packages/neon/neon/lib/src/widgets/account_switcher_button.dart @@ -7,6 +7,7 @@ import 'package:neon/src/pages/settings.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/utils/provider.dart'; import 'package:neon/src/widgets/account_selection_dialog.dart'; +import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; import 'package:neon/src/widgets/user_avatar.dart'; @internal @@ -23,7 +24,7 @@ class AccountSwitcherButton extends StatelessWidget { builder: (final context) => NeonAccountSelectionDialog( highlightActiveAccount: true, children: [ - ListTile( + AdaptiveListTile( leading: const Icon(Icons.settings), title: Text(NeonLocalizations.of(context).settingsAccountManage), onTap: () { diff --git a/packages/neon/neon/lib/src/widgets/account_tile.dart b/packages/neon/neon/lib/src/widgets/account_tile.dart index 421bfa9b..fec541f8 100644 --- a/packages/neon/neon/lib/src/widgets/account_tile.dart +++ b/packages/neon/neon/lib/src/widgets/account_tile.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:intersperse/intersperse.dart'; import 'package:meta/meta.dart'; @@ -5,6 +7,7 @@ import 'package:neon/src/bloc/result.dart'; import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/utils/provider.dart'; +import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; import 'package:neon/src/widgets/user_avatar.dart'; @@ -27,23 +30,11 @@ class NeonAccountTile extends StatelessWidget { /// {@endtemplate} final Account account; - /// {@template neon.AccountTile.trailing} - /// A widget to display after the title. - /// - /// Typically an [Icon] widget. - /// - /// To show right-aligned metadata (assuming left-to-right reading order; - /// left-aligned for right-to-left reading order), consider using a [Row] with - /// [CrossAxisAlignment.baseline] alignment whose first item is [Expanded] and - /// whose second child is the metadata text, instead of using the [trailing] - /// property. - /// {@endtemplate} + /// {@macro neon.AdaptiveListTile.trailing} final Widget? trailing; - /// {@template neon.AccountTile.onTap} - /// Called when the user taps this list tile. - /// {@endtemplate} - final GestureTapCallback? onTap; + /// {@macro neon.AdaptiveListTile.onTap} + final FutureOr Function()? onTap; /// Whether to also show the status on the avatar. /// @@ -55,7 +46,7 @@ class NeonAccountTile extends StatelessWidget { Widget build(final BuildContext context) { final userDetailsBloc = NeonProvider.of(context).getUserDetailsBlocFor(account); - return ListTile( + return AdaptiveListTile( onTap: onTap, leading: NeonUserAvatar( account: account, diff --git a/packages/neon/neon/lib/src/widgets/adaptive_widgets/list_tile.dart b/packages/neon/neon/lib/src/widgets/adaptive_widgets/list_tile.dart new file mode 100644 index 00000000..e0392fe7 --- /dev/null +++ b/packages/neon/neon/lib/src/widgets/adaptive_widgets/list_tile.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +/// A wrapper widget that adaptively displays a [ListTile] on Material platforms +/// and a [CupertinoListTile] on Cupertino ones. +class AdaptiveListTile extends StatelessWidget { + /// Creates a new adaptive list tile. + /// + /// If supplied the [subtitle] will be displayed below the title. + const AdaptiveListTile({ + required this.title, + this.enabled = true, + this.subtitle, + this.leading, + this.trailing, + this.onTap, + super.key, + }) : additionalInfo = null; + + /// Creates a new adaptive list tile. + /// + /// If supplied the [additionalInfo] will be displayed below the title on + /// Material platforms and as a trailing widget on Cupertino ones. + const AdaptiveListTile.additionalInfo({ + required this.title, + this.enabled = true, + this.additionalInfo, + this.leading, + this.trailing, + this.onTap, + super.key, + }) : subtitle = additionalInfo; + + /// {@template neon.AdaptiveListTile.title} + /// A [title] is used to convey the central information. Usually a [Text]. + /// {@endtemplate} + final Widget title; + + /// {@template neon.AdaptiveListTile.subtitle} + /// A [subtitle] is used to display additional information. It is located + /// below [title]. Usually a [Text] widget. + final Widget? subtitle; + + /// {@template neon.AdaptiveListTile.additionalInfo} + /// Similar to [subtitle], an [additionalInfo] is used to display additional + /// information. However, instead of being displayed below [title], it is + /// displayed on the right, before [trailing]. Usually a [Text] widget. + /// + /// This is only available on Cupertino platforms. + /// {@endtemplate} + final Widget? additionalInfo; + + /// {@template neon.AdaptiveListTile.leading} + /// A widget displayed at the start of the [AdaptiveListTile]. This is + /// typically an `Icon` or an `Image`. + /// {@endtemplate} + final Widget? leading; + + /// {@template neon.AdaptiveListTile.trailing} + /// A widget displayed at the end of the [AdaptiveListTile]. + /// {@endtemplate} + final Widget? trailing; + + /// {@template neon.AdaptiveListTile.onTap} + /// The [onTap] function is called when a user taps on the[AdaptiveListTile]. + /// If left `null`, the [AdaptiveListTile] will not react to taps. + /// + /// If the platform is a Cupertino one and this is a `Future Function()`, + /// then the [AdaptiveListTile] remains activated until the returned future is + /// awaited. This is according to iOS behavior. + /// However, if this function is a `void Function()`, then the tile is active + /// only for the duration of invocation. + /// {@endtemplate} + final FutureOr Function()? onTap; + + /// {@template neon.AdaptiveListTile.enabled} + /// Whether this list tile is interactive. + /// + /// If false, this list tile is styled with the disabled color from the + /// current [Theme] and the [onTap] callback is inoperative. + /// {@endtemplate} + final bool enabled; + + @override + Widget build(final BuildContext context) { + final theme = Theme.of(context); + + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return ListTile( + title: title, + subtitle: subtitle, + leading: leading, + trailing: trailing, + onTap: onTap, + enabled: enabled, + ); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + final tile = CupertinoListTile( + title: title, + subtitle: additionalInfo == null ? subtitle : null, + additionalInfo: additionalInfo, + leading: leading, + trailing: trailing, + onTap: enabled ? onTap : null, + ); + + if (!enabled) { + var data = CupertinoTheme.of(context); + data = data.copyWith( + textTheme: data.resolveFrom(context).textTheme.copyWith( + textStyle: data.textTheme.textStyle.merge( + TextStyle( + color: theme.disabledColor, + ), + ), + ), + ); + + return CupertinoTheme( + data: data, + child: tile, + ); + } + + return tile; + } + } +} diff --git a/packages/neon/neon/lib/src/widgets/error.dart b/packages/neon/neon/lib/src/widgets/error.dart index 18c87fd4..75d04e99 100644 --- a/packages/neon/neon/lib/src/widgets/error.dart +++ b/packages/neon/neon/lib/src/widgets/error.dart @@ -8,6 +8,7 @@ import 'package:neon/src/blocs/accounts.dart'; import 'package:neon/src/router.dart'; import 'package:neon/src/utils/exceptions.dart'; import 'package:neon/src/utils/provider.dart'; +import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:universal_io/io.dart'; @@ -22,7 +23,7 @@ enum NeonErrorType { /// Shows a column with the error message and a retry button. column, - /// Shows a [ListTile] with the error. + /// Shows a [AdaptiveListTile] with the error. listTile, } @@ -147,10 +148,9 @@ class NeonError extends StatelessWidget { ), ); case NeonErrorType.listTile: - return ListTile( + return AdaptiveListTile( leading: errorIcon, - title: Text(message), - titleTextStyle: textStyle, + title: Text(message, style: textStyle), onTap: onPressed, ); } diff --git a/packages/neon/neon/lib/src/widgets/unified_search_results.dart b/packages/neon/neon/lib/src/widgets/unified_search_results.dart index 473d8553..26949454 100644 --- a/packages/neon/neon/lib/src/widgets/unified_search_results.dart +++ b/packages/neon/neon/lib/src/widgets/unified_search_results.dart @@ -8,6 +8,7 @@ import 'package:neon/src/blocs/unified_search.dart'; import 'package:neon/src/models/account.dart'; import 'package:neon/src/theme/sizes.dart'; import 'package:neon/src/utils/provider.dart'; +import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; import 'package:neon/src/widgets/error.dart'; import 'package:neon/src/widgets/image.dart'; import 'package:neon/src/widgets/linear_progress_indicator.dart'; @@ -81,7 +82,7 @@ class NeonUnifiedSearchResults extends StatelessWidget { visible: result.isLoading, ), if (entries.isEmpty) ...[ - ListTile( + AdaptiveListTile( leading: const Icon( Icons.close, size: largeIconSize, @@ -90,7 +91,7 @@ class NeonUnifiedSearchResults extends StatelessWidget { ), ], for (final entry in entries) ...[ - ListTile( + AdaptiveListTile( leading: NeonImageWrapper( size: const Size.square(largeIconSize), child: _buildThumbnail(context, accountsBloc.activeAccount.value!, entry), diff --git a/packages/neon/neon/lib/src/widgets/validation_tile.dart b/packages/neon/neon/lib/src/widgets/validation_tile.dart index 543688fe..4f2aaf22 100644 --- a/packages/neon/neon/lib/src/widgets/validation_tile.dart +++ b/packages/neon/neon/lib/src/widgets/validation_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:neon/src/widgets/adaptive_widgets/list_tile.dart'; /// Validation list tile. /// @@ -48,7 +49,7 @@ class NeonValidationTile extends StatelessWidget { size: size, ), }; - return ListTile( + return AdaptiveListTile( leading: leading, title: Text( title,