From 8650158085ed2630e575669b0c3f5c97085ef9b8 Mon Sep 17 00:00:00 2001 From: Hamlet Jiang Su Date: Thu, 26 Feb 2026 07:42:27 -0800 Subject: [PATCH] feat: simplify ui package structure and clean up widget architecture --- .../content/content_action_handlers.dart | 25 - .../ui/src/models/content/content_media.dart | 61 - .../models/content/content_media_type.dart | 1 - .../src/models/content/content_view_mode.dart | 10 - .../ui/src/models/identity/avatar_data.dart | 13 - .../models/identity/identity_name_data.dart | 9 - .../utils/links/link_navigation_utils.dart | 41 - .../thunder_multi_action_dismissible.dart} | 107 +- .../actions/thunder_popup_menu_item.dart | 49 +- .../avatar.dart} | 47 +- .../widgets/avatar/models/avatar_data.dart | 20 + .../widgets/common/thunder_error_state.dart | 81 ++ .../widgets/common/thunder_icon_label.dart | 46 + .../src/widgets/content/content_renderer.dart | 19 - .../dialogs/thunder_typeahead_dialog.dart | 78 + .../widgets/fab/thunder_expandable_fab.dart | 325 +++++ .../widgets/identity/community_avatar.dart | 51 - .../src/widgets/identity/instance_avatar.dart | 18 - .../ui/src/widgets/identity/user_avatar.dart | 18 - .../widgets/layout/thunder_bottom_sheet.dart | 124 ++ .../src/widgets/markdown/markdown_body.dart | 3 - .../media/compact_thumbnail_preview.dart | 113 -- .../pickers/bottom_sheet_list_picker.dart | 5 +- .../ui/src/widgets/pickers/picker_item.dart | 138 +- .../settings/thunder_expandable_option.dart | 97 ++ .../widgets/settings/thunder_list_option.dart | 118 ++ .../settings/thunder_settings_tile.dart | 106 ++ .../settings/thunder_toggle_option.dart | 108 ++ lib/packages/ui/ui.dart | 52 +- .../app/bootstrap/preferences_migration.dart | 2 +- .../navigation/link_navigation_utils.dart | 2 +- lib/src/app/shell/pages/thunder_page.dart | 2 +- .../pages/create_comment_page.dart | 5 +- .../comment_card/additional_comment_card.dart | 4 +- .../widgets/comment_card/comment_card.dart | 12 +- .../comment_card_header.dart | 2 +- .../comment_card_header_date.dart | 4 +- .../comment_card_header_reply_count.dart | 4 +- .../comment_card_header_score.dart | 8 +- .../widgets/comment_card/comment_content.dart | 6 +- .../widgets/comment_reference.dart | 18 +- .../widgets/community_drawer.dart | 4 +- .../community_header/community_header.dart | 23 +- .../widgets/community_information.dart | 13 +- .../widgets/community_list_entry.dart | 11 +- .../presentation/widgets/post_card.dart | 13 +- .../widgets/post_card_metadata.dart | 87 +- .../widgets/post_card_view_comfortable.dart | 6 +- .../widgets/post_card_view_compact.dart | 2 +- .../widgets/common_markdown_body.dart | 84 -- .../widgets/media/media_utils.dart | 47 - .../widgets/media/media_view.dart | 176 --- .../feed/presentation/pages/feed_page.dart | 4 +- .../widgets/feed_page_app_bar.dart | 2 +- .../feed/presentation/widgets/tagline.dart | 2 +- .../widgets/avatars/community_avatar.dart | 53 - .../widgets/avatars/user_avatar.dart | 45 - .../widgets/full_name_widgets.dart | 133 -- .../widgets/text/scalable_text.dart | 39 - .../widgets/inbox_private_messages_view.dart | 24 +- .../widgets/instance_information.dart | 4 +- .../widgets/instance_list_entry.dart | 2 +- .../presentation/pages/report_page.dart | 24 +- .../widgets/modlog_filter_picker.dart | 4 +- .../widgets/modlog_item_card.dart | 6 +- .../widgets/modlog_item_context_card.dart | 65 +- .../utils/notification_settings_utils.dart | 2 +- .../presentation/pages/create_post_page.dart | 22 +- .../post/presentation/pages/post_page.dart | 4 +- .../presentation/utils/post_media_utils.dart | 4 +- .../presentation/widgets/cross_posts.dart | 22 +- .../widgets/post_body/post_body.dart | 8 +- .../widgets/post_body/post_body_preview.dart | 6 +- .../widgets/post_body/post_body_title.dart | 10 +- .../presentation/widgets/post_page_fab.dart | 7 +- .../theme_preferences_cubit.dart | 2 +- .../features/settings/domain/full_name.dart | 138 +- .../pages/accessibility_settings_page.dart | 23 +- .../comment_appearance_settings_page.dart | 160 +- .../post_appearance_settings_page.dart | 704 +++++---- .../pages/appearance/theme_settings_page.dart | 1095 +++++++------- .../pages/behavior/fab_settings_page.dart | 456 +++--- .../pages/behavior/filter_settings_page.dart | 91 +- .../pages/behavior/general_settings_page.dart | 1281 ++++++++--------- .../pages/behavior/gesture_settings_page.dart | 158 +- .../pages/behavior/video_player_settings.dart | 178 ++- .../pages/debug_settings_page.dart | 495 +++---- .../pages/user_labels_settings_page.dart | 12 +- .../utils/setting_link_utils.dart | 7 + .../widgets/accessibility_profile.dart | 9 +- .../widgets/action_color_setting_widget.dart | 508 +++---- .../widgets/expandable_option.dart | 86 -- .../presentation/widgets/list_option.dart | 147 -- .../widgets/settings_list_tile.dart | 116 -- .../presentation/widgets/toggle_option.dart | 187 --- .../presentation/widgets/widgets.dart | 4 - .../pages/media_management_page.dart | 18 +- .../pages/user_settings_block_page.dart | 22 +- .../pages/user_settings_page.dart | 586 ++++---- .../widgets/user_action_bottom_sheet.dart | 2 +- .../widgets/user_header/user_header.dart | 23 +- .../presentation/widgets/user_indicator.dart | 11 +- .../widgets/user_information.dart | 13 +- .../presentation/widgets/user_label_chip.dart | 4 +- .../presentation/widgets/user_list_entry.dart | 11 +- .../foundation/primitives/models/media.dart | 2 + .../utils/markdown/markdown_utils.dart | 0 .../content}/utils/media/media_utils.dart | 45 +- .../markdown/common_markdown_body.dart | 105 +- .../widgets/markdown/extended_markdown.dart | 0 .../widgets/markdown/markdown_lemmy_link.dart | 0 .../widgets/markdown/markdown_spoiler.dart | 2 +- .../markdown/markdown_subsuperscript.dart | 0 .../media/compact_thumbnail_preview.dart | 10 +- .../content}/widgets/media/image_preview.dart | 21 +- .../content}/widgets/media/image_viewer.dart | 4 +- .../widgets/media/link_information.dart | 15 +- .../content}/widgets/media/media_view.dart | 203 ++- .../widgets/media/media_view_text.dart | 0 lib/src/shared/icon_text.dart | 49 - .../shared/identity/models}/name_style.dart | 0 lib/src/shared/identity/utils/avatar_url.dart | 23 + .../identity/utils}/name_formatting.dart | 28 +- .../widgets/avatars/community_avatar.dart | 68 + .../widgets/avatars/instance_avatar.dart | 9 +- .../identity/widgets/avatars/user_avatar.dart | 37 + .../identity/widgets}/full_name_widgets.dart | 126 +- lib/src/shared/input_dialogs.dart | 117 +- lib/src/shared/link_information.dart | 118 -- lib/src/shared/links/links.dart | 1 - lib/src/shared/reply_to_preview_actions.dart | 15 +- .../shared/share/advanced_share_sheet.dart | 134 +- .../share_image_preview.dart} | 10 +- .../shared/widgets/chips/community_chip.dart | 18 +- lib/src/shared/widgets/chips/user_chip.dart | 18 +- .../shared/widgets/media/media_view_text.dart | 76 - .../widgets/text/selectable_text_modal.dart | 6 +- 137 files changed, 4924 insertions(+), 5793 deletions(-) delete mode 100644 lib/packages/ui/src/models/content/content_action_handlers.dart delete mode 100644 lib/packages/ui/src/models/content/content_media.dart delete mode 100644 lib/packages/ui/src/models/content/content_media_type.dart delete mode 100644 lib/packages/ui/src/models/content/content_view_mode.dart delete mode 100644 lib/packages/ui/src/models/identity/avatar_data.dart delete mode 100644 lib/packages/ui/src/models/identity/identity_name_data.dart delete mode 100644 lib/packages/ui/src/utils/links/link_navigation_utils.dart rename lib/{src/shared/widgets/multi_action_dismissible.dart => packages/ui/src/widgets/actions/thunder_multi_action_dismissible.dart} (56%) rename lib/packages/ui/src/widgets/{identity/avatar_widgets.dart => avatar/avatar.dart} (52%) create mode 100644 lib/packages/ui/src/widgets/avatar/models/avatar_data.dart create mode 100644 lib/packages/ui/src/widgets/common/thunder_error_state.dart create mode 100644 lib/packages/ui/src/widgets/common/thunder_icon_label.dart delete mode 100644 lib/packages/ui/src/widgets/content/content_renderer.dart create mode 100644 lib/packages/ui/src/widgets/dialogs/thunder_typeahead_dialog.dart create mode 100644 lib/packages/ui/src/widgets/fab/thunder_expandable_fab.dart delete mode 100644 lib/packages/ui/src/widgets/identity/community_avatar.dart delete mode 100644 lib/packages/ui/src/widgets/identity/instance_avatar.dart delete mode 100644 lib/packages/ui/src/widgets/identity/user_avatar.dart create mode 100644 lib/packages/ui/src/widgets/layout/thunder_bottom_sheet.dart delete mode 100644 lib/packages/ui/src/widgets/markdown/markdown_body.dart delete mode 100644 lib/packages/ui/src/widgets/media/compact_thumbnail_preview.dart create mode 100644 lib/packages/ui/src/widgets/settings/thunder_expandable_option.dart create mode 100644 lib/packages/ui/src/widgets/settings/thunder_list_option.dart create mode 100644 lib/packages/ui/src/widgets/settings/thunder_settings_tile.dart create mode 100644 lib/packages/ui/src/widgets/settings/thunder_toggle_option.dart delete mode 100644 lib/src/features/content/presentation/widgets/common_markdown_body.dart delete mode 100644 lib/src/features/content/presentation/widgets/media/media_utils.dart delete mode 100644 lib/src/features/content/presentation/widgets/media/media_view.dart delete mode 100644 lib/src/features/identity/presentation/widgets/avatars/community_avatar.dart delete mode 100644 lib/src/features/identity/presentation/widgets/avatars/user_avatar.dart delete mode 100644 lib/src/features/identity/presentation/widgets/full_name_widgets.dart delete mode 100644 lib/src/features/identity/presentation/widgets/text/scalable_text.dart delete mode 100644 lib/src/features/settings/presentation/widgets/expandable_option.dart delete mode 100644 lib/src/features/settings/presentation/widgets/list_option.dart delete mode 100644 lib/src/features/settings/presentation/widgets/settings_list_tile.dart delete mode 100644 lib/src/features/settings/presentation/widgets/toggle_option.dart rename lib/{packages/ui/src => src/shared/content}/utils/markdown/markdown_utils.dart (100%) rename lib/{packages/ui/src => src/shared/content}/utils/media/media_utils.dart (84%) rename lib/{packages/ui/src => src/shared/content}/widgets/markdown/common_markdown_body.dart (69%) rename lib/{packages/ui/src => src/shared/content}/widgets/markdown/extended_markdown.dart (100%) rename lib/{packages/ui/src => src/shared/content}/widgets/markdown/markdown_lemmy_link.dart (100%) rename lib/{packages/ui/src => src/shared/content}/widgets/markdown/markdown_spoiler.dart (98%) rename lib/{packages/ui/src => src/shared/content}/widgets/markdown/markdown_subsuperscript.dart (100%) rename lib/src/{features/content/presentation => shared/content}/widgets/media/compact_thumbnail_preview.dart (85%) rename lib/{packages/ui/src => src/shared/content}/widgets/media/image_preview.dart (95%) rename lib/{packages/ui/src => src/shared/content}/widgets/media/image_viewer.dart (99%) rename lib/{packages/ui/src => src/shared/content}/widgets/media/link_information.dart (80%) rename lib/{packages/ui/src => src/shared/content}/widgets/media/media_view.dart (63%) rename lib/{packages/ui/src => src/shared/content}/widgets/media/media_view_text.dart (100%) delete mode 100644 lib/src/shared/icon_text.dart rename lib/{packages/ui/src/models/identity => src/shared/identity/models}/name_style.dart (100%) create mode 100644 lib/src/shared/identity/utils/avatar_url.dart rename lib/{packages/ui/src/utils/identity => src/shared/identity/utils}/name_formatting.dart (61%) create mode 100644 lib/src/shared/identity/widgets/avatars/community_avatar.dart rename lib/src/{features/identity/presentation => shared/identity}/widgets/avatars/instance_avatar.dart (69%) create mode 100644 lib/src/shared/identity/widgets/avatars/user_avatar.dart rename lib/{packages/ui/src/widgets/identity => src/shared/identity/widgets}/full_name_widgets.dart (56%) delete mode 100644 lib/src/shared/link_information.dart delete mode 100644 lib/src/shared/links/links.dart rename lib/src/shared/{image_preview.dart => share/share_image_preview.dart} (96%) delete mode 100644 lib/src/shared/widgets/media/media_view_text.dart diff --git a/lib/packages/ui/src/models/content/content_action_handlers.dart b/lib/packages/ui/src/models/content/content_action_handlers.dart deleted file mode 100644 index 505e0a4e9..000000000 --- a/lib/packages/ui/src/models/content/content_action_handlers.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/widgets.dart'; - -typedef OpenLinkHandler = void Function(BuildContext context, String url); -typedef OpenImageHandler = void Function(BuildContext context, {String? url, Uint8List? bytes}); -typedef OpenVideoHandler = void Function(BuildContext context, String url); -typedef MarkReadHandler = void Function(int? postId); -typedef LongPressLinkHandler = void Function(BuildContext context, String text, String? url); - -class ContentActionHandlers { - const ContentActionHandlers({ - this.onOpenLink, - this.onLongPressLink, - this.onOpenImage, - this.onOpenVideo, - this.onMarkRead, - }); - - final OpenLinkHandler? onOpenLink; - final LongPressLinkHandler? onLongPressLink; - final OpenImageHandler? onOpenImage; - final OpenVideoHandler? onOpenVideo; - final MarkReadHandler? onMarkRead; -} diff --git a/lib/packages/ui/src/models/content/content_media.dart b/lib/packages/ui/src/models/content/content_media.dart deleted file mode 100644 index 197ced85d..000000000 --- a/lib/packages/ui/src/models/content/content_media.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:thunder/packages/ui/src/models/content/content_media_type.dart'; - -/// Generic media model used by content widgets. -class ContentMedia { - ContentMedia({ - this.thumbnailUrl, - this.mediaUrl, - this.originalUrl, - this.width, - this.height, - this.nsfw = false, - required this.mediaType, - this.altText, - this.contentType, - }); - - /// The original external URL of the post. - String? originalUrl; - - /// The thumbnail URL of the media. - String? thumbnailUrl; - - /// The actual URL of the media source. - String? mediaUrl; - - /// The width of the media source. - double? width; - - /// The height of the media source. - double? height; - - /// Indicates whether the media is NSFW. - bool nsfw; - - /// Indicates the type of media it holds. - ContentMediaType mediaType; - - /// Includes an alternative text-based description of the image. - String? altText; - - /// The content type of the media. - String? contentType; - - /// Gets the full-size image URL, if any. - String? get imageUrl => _looksLikeImage(mediaUrl) ? mediaUrl : thumbnailUrl; - - bool _looksLikeImage(String? url) { - if (url == null) return false; - if (url.contains('/image_proxy')) return true; - - final lowerPath = (Uri.tryParse(url)?.path ?? url).toLowerCase(); - return lowerPath.endsWith('.png') || - lowerPath.endsWith('.jpg') || - lowerPath.endsWith('.jpeg') || - lowerPath.endsWith('.gif') || - lowerPath.endsWith('.bmp') || - lowerPath.endsWith('.webp') || - lowerPath.endsWith('.avif') || - lowerPath.endsWith('@jpeg'); - } -} diff --git a/lib/packages/ui/src/models/content/content_media_type.dart b/lib/packages/ui/src/models/content/content_media_type.dart deleted file mode 100644 index 83875b6e4..000000000 --- a/lib/packages/ui/src/models/content/content_media_type.dart +++ /dev/null @@ -1 +0,0 @@ -enum ContentMediaType { image, video, link, text } diff --git a/lib/packages/ui/src/models/content/content_view_mode.dart b/lib/packages/ui/src/models/content/content_view_mode.dart deleted file mode 100644 index 666aae203..000000000 --- a/lib/packages/ui/src/models/content/content_view_mode.dart +++ /dev/null @@ -1,10 +0,0 @@ -enum ContentViewMode { - comment(150.0), - compact(75.0), - comfortable(150.0); - - /// The height of media previews for the given view mode. - final double height; - - const ContentViewMode(this.height); -} diff --git a/lib/packages/ui/src/models/identity/avatar_data.dart b/lib/packages/ui/src/models/identity/avatar_data.dart deleted file mode 100644 index e17c72771..000000000 --- a/lib/packages/ui/src/models/identity/avatar_data.dart +++ /dev/null @@ -1,13 +0,0 @@ -class AvatarData { - const AvatarData({ - required this.fallbackLabel, - this.imageUrl, - this.radius = 16.0, - this.semanticLabel, - }); - - final String fallbackLabel; - final String? imageUrl; - final double radius; - final String? semanticLabel; -} diff --git a/lib/packages/ui/src/models/identity/identity_name_data.dart b/lib/packages/ui/src/models/identity/identity_name_data.dart deleted file mode 100644 index 1864a7fae..000000000 --- a/lib/packages/ui/src/models/identity/identity_name_data.dart +++ /dev/null @@ -1,9 +0,0 @@ -class IdentityNameData { - const IdentityNameData({ - required this.primary, - this.secondary, - }); - - final String primary; - final String? secondary; -} diff --git a/lib/packages/ui/src/utils/links/link_navigation_utils.dart b/lib/packages/ui/src/utils/links/link_navigation_utils.dart deleted file mode 100644 index 0ac5a8cbb..000000000 --- a/lib/packages/ui/src/utils/links/link_navigation_utils.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:intl/message_format.dart'; - -/// Resolves the best URL from markdown link text and target. -String resolveMarkdownLink(String text, String? url) { - final parsedUri = Uri.tryParse(url ?? '') ?? Uri.tryParse(text); - - String parsedUrl = text; - - if (parsedUri != null && parsedUri.host.isNotEmpty) { - parsedUrl = parsedUri.toString(); - } else { - parsedUrl = url ?? ''; - } - - // The markdown link processor treats URLs with @ as emails and prepends - // `mailto:`. If the displayed text does not include that prefix, remove it. - if (parsedUrl.startsWith('mailto:') && !text.startsWith('mailto:')) { - parsedUrl = parsedUrl.replaceFirst('mailto:', ''); - } - - return parsedUrl; -} - -List<({String sourceName, String link})> generateAlternateSources(String link) { - return _alternateSources.map((alternateSource) { - return (sourceName: alternateSource.sourceName, link: alternateSource.template.format({'link': link})); - }).toList(); -} - -final List<({String sourceName, MessageFormat template})> _alternateSources = [ - (sourceName: 'Archive Today', template: MessageFormat('https://archive.today/{link}')), - (sourceName: 'Internet Archive', template: MessageFormat('https://web.archive.org/save/{link}')), - (sourceName: 'Ground News', template: MessageFormat('https://ground.news/find?url={link}')), -]; - -/// Determines if a given URL is valid. The URL must have the `http` or -/// `https` scheme. -bool isValidUrl(String url) { - final uri = Uri.tryParse(url); - return uri != null && uri.hasAbsolutePath && uri.scheme.startsWith('http'); -} diff --git a/lib/src/shared/widgets/multi_action_dismissible.dart b/lib/packages/ui/src/widgets/actions/thunder_multi_action_dismissible.dart similarity index 56% rename from lib/src/shared/widgets/multi_action_dismissible.dart rename to lib/packages/ui/src/widgets/actions/thunder_multi_action_dismissible.dart index 22d960a5e..b5c324c98 100644 --- a/lib/src/shared/widgets/multi_action_dismissible.dart +++ b/lib/packages/ui/src/widgets/actions/thunder_multi_action_dismissible.dart @@ -1,20 +1,27 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:thunder/src/features/settings/domain/swipe_action.dart'; - -typedef SwipeBackgroundBuilder = Widget Function( +typedef ThunderSwipeBackgroundBuilder = Widget Function( BuildContext context, DismissDirection effectiveDirection, double progress, - SwipeAction? action, + ThunderSwipeAction? action, ); -typedef SwipeIconResolver = IconData? Function(SwipeAction action); +class ThunderSwipeAction { + const ThunderSwipeAction({ + required this.value, + this.icon, + required this.color, + }); + + final T value; + final IconData? icon; + final Color Function(BuildContext context) color; +} -/// A dismissible widget that supports multiple swipe actions. -class MultiActionDismissible extends StatefulWidget { - const MultiActionDismissible({ +class ThunderMultiActionDismissible extends StatefulWidget { + const ThunderMultiActionDismissible({ super.key, required this.child, required this.direction, @@ -31,63 +38,30 @@ class MultiActionDismissible extends StatefulWidget { this.backgroundMaxWidthFactor = 1.0, }); - /// The content of the dismissible widget. final Widget child; - - /// The allowed swipe directions. final DismissDirection direction; - - /// The thresholds (0.0 - 1.0 of width) at which successive actions should trigger. - /// - /// For example: [0.15, 0.35, 0.6] supports up to 3 actions. - /// If there are more thresholds than actions on a side, the last action repeats for further thresholds. - /// If empty, no actions will be triggered. final List actionThresholds; - - /// The actions to be displayed on the left side of the widget, ordered from shortest to longest swipe. - final List leftActions; - - /// The actions to be displayed on the right side of the widget, ordered from shortest to longest swipe. - final List rightActions; - - /// The action to be performed when the user releases the widget. - final void Function(SwipeAction action)? onAction; - - /// A callback that is called when the progress changes. - final void Function(double progress, DismissDirection direction, SwipeAction? action)? onProgressChanged; - - /// A callback that is called when the user presses down on the widget. + final List> leftActions; + final List> rightActions; + final void Function(ThunderSwipeAction action)? onAction; + final void Function(double progress, DismissDirection direction, ThunderSwipeAction? action)? onProgressChanged; final VoidCallback? onPointerDown; - - /// A callback that is called when the user releases the widget. final void Function(double verticalDelta)? onDragEnd; - - /// Whether to produce haptic feedback when the action changes. final bool enableHaptics; - - /// Whether to temporarily disable swipe. This is used to allow the system back gesture to work when the user is swiping right. final bool enableBackSwipeOverride; - - /// A custom background builder. If not provided, a default will be used. - final SwipeBackgroundBuilder? backgroundBuilder; - - /// The max width fraction used by the default background. + final ThunderSwipeBackgroundBuilder? backgroundBuilder; final double backgroundMaxWidthFactor; @override - State createState() => _MultiActionDismissibleState(); + State> createState() => _ThunderMultiActionDismissibleState(); } -class _MultiActionDismissibleState extends State { - double _progress = 0.0; - SwipeAction? _currentAction; +class _ThunderMultiActionDismissibleState extends State> { + double _progress = 0; + ThunderSwipeAction? _currentAction; DismissDirection _currentDirection = DismissDirection.startToEnd; bool _overrideSwipe = false; - double _lastVerticalDelta = 0.0; - - void _handlePointerDown() { - widget.onPointerDown?.call(); - } + double _lastVerticalDelta = 0; void _handlePointerMove(PointerMoveEvent event) { _lastVerticalDelta = event.delta.dy; @@ -95,9 +69,9 @@ class _MultiActionDismissibleState extends State { if (!widget.enableBackSwipeOverride) return; if (widget.direction != DismissDirection.endToStart) return; - final bool isSwipingRight = event.delta.dx > 0; + final isSwipingRight = event.delta.dx > 0; - if (isSwipingRight && !_overrideSwipe && _progress == 0.0) { + if (isSwipingRight && !_overrideSwipe && _progress == 0) { setState(() => _overrideSwipe = true); } else if (!isSwipingRight && _overrideSwipe) { setState(() => _overrideSwipe = false); @@ -106,7 +80,7 @@ class _MultiActionDismissibleState extends State { void _handlePointerUp() { if (_overrideSwipe) setState(() => _overrideSwipe = false); - if (_currentAction != null && _currentAction != SwipeAction.none) widget.onAction?.call(_currentAction!); + if (_currentAction != null) widget.onAction?.call(_currentAction!); widget.onDragEnd?.call(_lastVerticalDelta); } @@ -114,8 +88,8 @@ class _MultiActionDismissibleState extends State { final progress = details.progress; final dir = details.direction; - SwipeAction? next; - final bool isStartToEnd = dir == DismissDirection.startToEnd; + ThunderSwipeAction? next; + final isStartToEnd = dir == DismissDirection.startToEnd; if (widget.actionThresholds.isNotEmpty && progress > widget.actionThresholds.first) { int tierIndex = 0; for (int i = 0; i < widget.actionThresholds.length; i++) { @@ -125,16 +99,17 @@ class _MultiActionDismissibleState extends State { break; } } - final List actions = isStartToEnd ? widget.leftActions : widget.rightActions; + + final actions = isStartToEnd ? widget.leftActions : widget.rightActions; if (actions.isNotEmpty) { - final int actionIndex = tierIndex.clamp(0, actions.length - 1); + final actionIndex = tierIndex.clamp(0, actions.length - 1); next = actions[actionIndex]; } } else { next = null; } - final bool actionChanged = next != _currentAction && next != null; + final actionChanged = next != _currentAction && next != null; setState(() { _progress = progress; @@ -152,13 +127,14 @@ class _MultiActionDismissibleState extends State { Widget _buildDefaultBackground(BuildContext context) { final alignment = _currentDirection == DismissDirection.startToEnd ? Alignment.centerLeft : Alignment.centerRight; final actions = _currentDirection == DismissDirection.startToEnd ? widget.leftActions : widget.rightActions; - final fallback = actions.isNotEmpty ? actions.first : SwipeAction.none; - final defaultColor = fallback.getColor(context); - final double leadingThreshold = widget.actionThresholds.isNotEmpty ? widget.actionThresholds.first : 1.0; - final backgroundColor = _currentAction != null ? _currentAction!.getColor(context) : defaultColor.withValues(alpha: leadingThreshold == 0 ? 0 : (_progress / leadingThreshold).clamp(0.0, 1.0)); + final fallback = actions.isNotEmpty ? actions.first : null; + + final leadingThreshold = widget.actionThresholds.isNotEmpty ? widget.actionThresholds.first : 1.0; + final defaultColor = fallback?.color(context) ?? Theme.of(context).colorScheme.primaryContainer; + final backgroundColor = _currentAction != null ? _currentAction!.color(context) : defaultColor.withValues(alpha: leadingThreshold == 0 ? 0 : (_progress / leadingThreshold).clamp(0.0, 1.0)); final width = MediaQuery.of(context).size.width * widget.backgroundMaxWidthFactor * _progress; - final icon = _currentAction?.getIcon(); + final icon = _currentAction?.icon; return AnimatedContainer( alignment: alignment, @@ -174,7 +150,6 @@ class _MultiActionDismissibleState extends State { @override Widget build(BuildContext context) { final disabled = widget.direction == DismissDirection.none; - Widget content = widget.child; if (!disabled) { @@ -195,7 +170,7 @@ class _MultiActionDismissibleState extends State { return Listener( behavior: HitTestBehavior.opaque, - onPointerDown: (_) => _handlePointerDown(), + onPointerDown: (_) => widget.onPointerDown?.call(), onPointerMove: _handlePointerMove, onPointerUp: (_) => _handlePointerUp(), child: content, diff --git a/lib/packages/ui/src/widgets/actions/thunder_popup_menu_item.dart b/lib/packages/ui/src/widgets/actions/thunder_popup_menu_item.dart index 14134ed16..01faeceac 100644 --- a/lib/packages/ui/src/widgets/actions/thunder_popup_menu_item.dart +++ b/lib/packages/ui/src/widgets/actions/thunder_popup_menu_item.dart @@ -1,24 +1,25 @@ -import 'package:flutter/material.dart'; - -/// Defines a custom [PopupMenuItem] that can be used throughout the app -class ThunderPopupMenuItem extends PopupMenuItem { - final IconData icon; - final String title; - final Widget? trailing; - - ThunderPopupMenuItem({ - super.key, - required super.onTap, - required this.icon, - required this.title, - this.trailing, - }) : super( - child: ListTile( - dense: true, - horizontalTitleGap: 5, - leading: Icon(icon, size: 20), - title: Text(title), - trailing: trailing, - ), - ); -} +import 'package:flutter/material.dart'; + +/// Defines a custom [PopupMenuItem] that can be used throughout the app +class ThunderPopupMenuItem extends PopupMenuItem { + final IconData icon; + final String title; + final Widget? trailing; + + ThunderPopupMenuItem({ + super.key, + super.value, + required super.onTap, + required this.icon, + required this.title, + this.trailing, + }) : super( + child: ListTile( + dense: true, + horizontalTitleGap: 5, + leading: Icon(icon, size: 20), + title: Text(title), + trailing: trailing, + ), + ); +} diff --git a/lib/packages/ui/src/widgets/identity/avatar_widgets.dart b/lib/packages/ui/src/widgets/avatar/avatar.dart similarity index 52% rename from lib/packages/ui/src/widgets/identity/avatar_widgets.dart rename to lib/packages/ui/src/widgets/avatar/avatar.dart index 0966e6410..f72a840cb 100644 --- a/lib/packages/ui/src/widgets/identity/avatar_widgets.dart +++ b/lib/packages/ui/src/widgets/avatar/avatar.dart @@ -2,59 +2,20 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:thunder/packages/ui/src/models/identity/avatar_data.dart'; +import 'package:thunder/packages/ui/src/widgets/avatar/models/avatar_data.dart'; -class UserAvatarWidget extends StatelessWidget { - const UserAvatarWidget({ +class Avatar extends StatelessWidget { + const Avatar({ super.key, required this.data, }); final AvatarData data; - @override - Widget build(BuildContext context) { - return _AvatarWidget(data: data); - } -} - -class CommunityAvatarWidget extends StatelessWidget { - const CommunityAvatarWidget({ - super.key, - required this.data, - }); - - final AvatarData data; - - @override - Widget build(BuildContext context) { - return _AvatarWidget(data: data); - } -} - -class InstanceAvatarWidget extends StatelessWidget { - const InstanceAvatarWidget({ - super.key, - required this.data, - }); - - final AvatarData data; - - @override - Widget build(BuildContext context) { - return _AvatarWidget(data: data); - } -} - -class _AvatarWidget extends StatelessWidget { - const _AvatarWidget({required this.data}); - - final AvatarData data; - @override Widget build(BuildContext context) { final theme = Theme.of(context); - final fallbackText = data.fallbackLabel.isNotEmpty ? data.fallbackLabel[0].toUpperCase() : ''; + final fallbackText = data.fallbackLabel?.isNotEmpty == true ? data.fallbackLabel![0].toUpperCase() : ''; final placeholder = CircleAvatar( backgroundColor: theme.colorScheme.secondaryContainer, diff --git a/lib/packages/ui/src/widgets/avatar/models/avatar_data.dart b/lib/packages/ui/src/widgets/avatar/models/avatar_data.dart new file mode 100644 index 000000000..a5c968a9a --- /dev/null +++ b/lib/packages/ui/src/widgets/avatar/models/avatar_data.dart @@ -0,0 +1,20 @@ +class AvatarData { + const AvatarData({ + this.imageUrl, + this.radius = 16.0, + this.fallbackLabel, + this.semanticLabel, + }); + + /// The URL of the avatar image. + final String? imageUrl; + + /// The radius of the avatar. + final double radius; + + /// The label to display when the avatar is not available. + final String? fallbackLabel; + + /// The semantic label of the avatar. + final String? semanticLabel; +} diff --git a/lib/packages/ui/src/widgets/common/thunder_error_state.dart b/lib/packages/ui/src/widgets/common/thunder_error_state.dart new file mode 100644 index 000000000..740f8d97e --- /dev/null +++ b/lib/packages/ui/src/widgets/common/thunder_error_state.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +class ThunderErrorAction { + const ThunderErrorAction({ + required this.label, + required this.onPressed, + this.loading = false, + this.primary = false, + }); + + final String label; + final VoidCallback onPressed; + final bool loading; + final bool primary; +} + +class ThunderErrorState extends StatelessWidget { + const ThunderErrorState({ + super.key, + this.title, + this.message, + this.actions = const [], + this.icon, + }); + + final String? title; + final String? message; + final List actions; + final IconData? icon; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Center( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(icon ?? Icons.warning_rounded, size: 100, color: theme.colorScheme.error), + const SizedBox(height: 32), + Text( + title ?? 'Something went wrong', + style: theme.textTheme.titleLarge, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + message ?? 'An unexpected error occurred.', + style: theme.textTheme.labelLarge?.copyWith(color: theme.dividerColor), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + if (actions.isNotEmpty) + Column( + children: [ + for (int i = 0; i < actions.length; i++) ...[ + SizedBox( + width: double.infinity, + child: actions[i].primary || i == 0 + ? ElevatedButton( + onPressed: actions[i].loading ? null : actions[i].onPressed, + child: actions[i].loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) : Text(actions[i].label), + ) + : TextButton( + onPressed: actions[i].loading ? null : actions[i].onPressed, + child: actions[i].loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) : Text(actions[i].label), + ), + ), + if (i != actions.length - 1) const SizedBox(height: 12), + ], + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/packages/ui/src/widgets/common/thunder_icon_label.dart b/lib/packages/ui/src/widgets/common/thunder_icon_label.dart new file mode 100644 index 000000000..1adce7931 --- /dev/null +++ b/lib/packages/ui/src/widgets/common/thunder_icon_label.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'package:thunder/packages/ui/src/widgets/identity/scalable_text.dart'; + +class ThunderIconLabel extends StatelessWidget { + const ThunderIconLabel({ + super.key, + required this.icon, + this.label, + this.labelStyle, + this.textScaleFactor = 1.0, + this.semanticsLabel, + this.gap = 4, + this.mainAxisSize = MainAxisSize.min, + }); + + final Widget icon; + final String? label; + final TextStyle? labelStyle; + final double textScaleFactor; + final String? semanticsLabel; + final double gap; + final MainAxisSize mainAxisSize; + + @override + Widget build(BuildContext context) { + if (label == null || label!.isEmpty) return icon; + + final theme = Theme.of(context); + + return Row( + mainAxisSize: mainAxisSize, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + icon, + SizedBox(width: gap), + ScalableText( + label!, + style: labelStyle ?? theme.textTheme.bodyMedium, + textScaleFactor: textScaleFactor, + semanticsLabel: semanticsLabel, + ), + ], + ); + } +} diff --git a/lib/packages/ui/src/widgets/content/content_renderer.dart b/lib/packages/ui/src/widgets/content/content_renderer.dart deleted file mode 100644 index f8608937e..000000000 --- a/lib/packages/ui/src/widgets/content/content_renderer.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import 'package:thunder/packages/ui/src/models/content/content_action_handlers.dart'; - -class ContentRenderer extends StatelessWidget { - const ContentRenderer({ - super.key, - required this.builder, - this.handlers = const ContentActionHandlers(), - }); - - final Widget Function(BuildContext context, ContentActionHandlers handlers) builder; - final ContentActionHandlers handlers; - - @override - Widget build(BuildContext context) { - return builder(context, handlers); - } -} diff --git a/lib/packages/ui/src/widgets/dialogs/thunder_typeahead_dialog.dart b/lib/packages/ui/src/widgets/dialogs/thunder_typeahead_dialog.dart new file mode 100644 index 000000000..94f87947b --- /dev/null +++ b/lib/packages/ui/src/widgets/dialogs/thunder_typeahead_dialog.dart @@ -0,0 +1,78 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_typeahead/flutter_typeahead.dart'; + +import 'package:thunder/packages/ui/src/widgets/dialogs/thunder_dialog.dart'; + +Future showThunderTypeaheadDialog({ + required BuildContext context, + required String title, + required String inputLabel, + required String primaryButtonText, + required String secondaryButtonText, + required Future Function({T? payload, String? value}) onSubmitted, + required FutureOr?> Function(String query) getSuggestions, + required Widget Function(T payload) suggestionBuilder, +}) async { + final textController = TextEditingController(); + StateSetter? contentWidgetSetState; + String? contentWidgetError; + + await showThunderDialog( + context: context, + title: title, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: secondaryButtonText, + primaryButtonInitialEnabled: false, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) async { + setPrimaryButtonEnabled(false); + final submitError = await onSubmitted(value: textController.text); + contentWidgetSetState?.call(() => contentWidgetError = submitError); + }, + primaryButtonText: primaryButtonText, + contentWidgetBuilder: (setPrimaryButtonEnabled) => StatefulBuilder( + builder: (context, setState) { + contentWidgetSetState = setState; + + return SizedBox( + width: min(MediaQuery.of(context).size.width, 700), + child: TypeAheadField( + controller: textController, + builder: (context, controller, focusNode) => TextField( + controller: controller, + focusNode: focusNode, + onChanged: (value) { + setPrimaryButtonEnabled(value.trim().isNotEmpty); + setState(() => contentWidgetError = null); + }, + autofocus: true, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: inputLabel, + errorText: contentWidgetError, + ), + onSubmitted: (text) async { + setPrimaryButtonEnabled(false); + final submitError = await onSubmitted(value: text); + setState(() => contentWidgetError = submitError); + }, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, payload) => suggestionBuilder(payload), + onSelected: (payload) async { + setPrimaryButtonEnabled(false); + final submitError = await onSubmitted(payload: payload); + setState(() => contentWidgetError = submitError); + }, + hideOnEmpty: true, + hideOnLoading: true, + hideOnError: true, + ), + ); + }, + ), + ); +} diff --git a/lib/packages/ui/src/widgets/fab/thunder_expandable_fab.dart b/lib/packages/ui/src/widgets/fab/thunder_expandable_fab.dart new file mode 100644 index 000000000..d53385d81 --- /dev/null +++ b/lib/packages/ui/src/widgets/fab/thunder_expandable_fab.dart @@ -0,0 +1,325 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class ThunderFabActionButton extends StatelessWidget { + const ThunderFabActionButton({ + super.key, + this.onPressed, + required this.icon, + this.label, + this.backgroundColor, + this.compact = false, + }); + + final VoidCallback? onPressed; + final Icon icon; + final String? label; + final Color? backgroundColor; + final bool compact; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + if (compact) { + return SizedBox( + width: 160, + child: Material( + color: Colors.transparent, + elevation: 3, + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(16)), + onTap: onPressed, + child: SizedBox( + height: 40, + child: Row( + children: [ + const SizedBox(width: 12), + Icon(icon.icon, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + label ?? '', + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + const SizedBox(width: 12), + ], + ), + ), + ), + ), + ); + } + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (label != null) Text(label!), + if (label != null) const SizedBox(width: 16), + SizedBox( + height: 40, + width: 40, + child: Material( + borderRadius: const BorderRadius.all(Radius.circular(8)), + clipBehavior: Clip.antiAlias, + color: backgroundColor ?? theme.colorScheme.primaryContainer, + elevation: 4, + child: InkWell(onTap: onPressed, child: icon), + ), + ), + ], + ); + } +} + +class ThunderExpandableFab extends StatefulWidget { + const ThunderExpandableFab({ + super.key, + this.initialOpen = false, + required this.distance, + required this.children, + required this.icon, + this.onSlideUp, + this.onSlideLeft, + this.onSlideDown, + this.onPressed, + this.onLongPress, + this.centered = false, + this.heroTag, + this.fabBackgroundColor, + this.onOpenChanged, + }); + + final bool initialOpen; + final double distance; + final List children; + final Icon icon; + final VoidCallback? onSlideUp; + final VoidCallback? onSlideLeft; + final VoidCallback? onSlideDown; + final VoidCallback? onPressed; + final VoidCallback? onLongPress; + final bool centered; + final String? heroTag; + final Color? fabBackgroundColor; + final ValueChanged? onOpenChanged; + + @override + State createState() => _ThunderExpandableFabState(); +} + +class _ThunderExpandableFabState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _expandAnimation; + late bool _isOpen; + + @override + void initState() { + super.initState(); + _isOpen = widget.initialOpen; + _controller = AnimationController( + value: _isOpen ? 1.0 : 0.0, + duration: const Duration(milliseconds: 250), + vsync: this, + ); + _expandAnimation = CurvedAnimation( + curve: Curves.fastOutSlowIn, + reverseCurve: Curves.easeOutQuad, + parent: _controller, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _setOpen(bool open) { + if (_isOpen == open) return; + setState(() => _isOpen = open); + if (open) { + _controller.forward(); + } else { + _controller.reverse(); + } + widget.onOpenChanged?.call(open); + } + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Stack( + alignment: widget.centered ? Alignment.bottomCenter : Alignment.bottomRight, + clipBehavior: Clip.none, + children: [ + _buildTapToCloseFab(), + ..._buildExpandingActionButtons(), + _buildTapToOpenFab(), + ], + ), + ); + } + + Widget _buildTapToCloseFab() { + return SizedBox( + width: widget.centered ? 45 : 56, + height: widget.centered ? 45 : 56, + child: AnimatedBuilder( + animation: _expandAnimation, + builder: (context, child) => child!, + child: FadeTransition( + opacity: _expandAnimation, + child: Center( + child: Material( + shape: widget.centered ? null : const CircleBorder(), + clipBehavior: widget.centered ? Clip.none : Clip.antiAlias, + color: widget.centered ? Colors.transparent : null, + elevation: widget.centered ? 0 : 4, + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () => _setOpen(false), + child: Padding( + padding: EdgeInsets.all(widget.centered ? 12 : 8), + child: Icon( + Icons.close, + size: widget.centered ? 20 : 25, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + ), + ), + ), + ), + ), + ), + ); + } + + List _buildExpandingActionButtons() { + final children = []; + final count = widget.children.length; + + for (var i = 0, distance = widget.distance; i < count; i++, distance += widget.distance) { + children.add( + _ThunderExpandingActionButton( + maxDistance: distance, + progress: _expandAnimation, + centered: widget.centered, + child: widget.children[i], + ), + ); + } + + return children; + } + + Widget _buildTapToOpenFab() { + return IgnorePointer( + ignoring: _isOpen, + child: AnimatedContainer( + transformAlignment: Alignment.center, + transform: Matrix4.diagonal3Values(_isOpen ? 0.7 : 1, _isOpen ? 0.7 : 1, 1), + duration: const Duration(milliseconds: 250), + curve: const Interval(0, 0.5, curve: Curves.easeOut), + child: AnimatedOpacity( + opacity: _isOpen ? 0 : 1, + curve: const Interval(0.25, 1, curve: Curves.easeInOut), + duration: const Duration(milliseconds: 250), + child: GestureDetector( + onVerticalDragUpdate: (details) { + if (details.delta.dy < -5) { + _setOpen(true); + widget.onSlideUp?.call(); + } + if (details.delta.dy > 5) { + widget.onSlideDown?.call(); + } + }, + onHorizontalDragUpdate: (details) { + if (details.delta.dx < -5) widget.onSlideLeft?.call(); + }, + onLongPress: () { + HapticFeedback.heavyImpact(); + widget.onLongPress?.call(); + }, + onTapDown: (_) => HapticFeedback.mediumImpact(), + child: widget.centered + ? SizedBox( + width: 45, + height: 45, + child: Material( + clipBehavior: Clip.antiAlias, + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () { + HapticFeedback.mediumImpact(); + widget.onPressed?.call(); + }, + child: Icon(widget.icon.icon, size: 20, semanticLabel: widget.icon.semanticLabel), + ), + ), + ) + : FloatingActionButton( + heroTag: widget.heroTag, + backgroundColor: widget.fabBackgroundColor, + onPressed: widget.onPressed, + child: widget.icon, + ), + ), + ), + ), + ); + } +} + +@immutable +class _ThunderExpandingActionButton extends StatefulWidget { + const _ThunderExpandingActionButton({ + required this.maxDistance, + required this.progress, + required this.child, + this.centered = false, + }); + + final double maxDistance; + final Animation progress; + final Widget child; + final bool centered; + + @override + State<_ThunderExpandingActionButton> createState() => _ThunderExpandingActionButtonState(); +} + +class _ThunderExpandingActionButtonState extends State<_ThunderExpandingActionButton> { + bool _visible = false; + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: widget.progress, + builder: (context, child) { + final offset = Offset.fromDirection( + 90 * (math.pi / 180.0), + widget.progress.value * widget.maxDistance, + ); + _visible = !widget.progress.isDismissed; + + return Visibility( + visible: _visible, + child: Positioned( + right: widget.centered ? null : 8 + offset.dx, + bottom: (widget.centered ? 15 : 10) + offset.dy, + child: child!, + ), + ); + }, + child: FadeTransition(opacity: widget.progress, child: widget.child), + ); + } +} diff --git a/lib/packages/ui/src/widgets/identity/community_avatar.dart b/lib/packages/ui/src/widgets/identity/community_avatar.dart deleted file mode 100644 index 17adf1943..000000000 --- a/lib/packages/ui/src/widgets/identity/community_avatar.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/packages/ui/src/models/identity/avatar_data.dart'; -import 'package:thunder/packages/ui/src/widgets/identity/avatar_widgets.dart'; - -class CommunityAvatar extends StatelessWidget { - const CommunityAvatar({ - super.key, - required this.data, - this.showRestrictedBadge = false, - this.restrictedBadgeTooltip, - this.restrictedBadgeSemanticLabel, - }); - - final AvatarData data; - final bool showRestrictedBadge; - final String? restrictedBadgeTooltip; - final String? restrictedBadgeSemanticLabel; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return Stack( - children: [ - CommunityAvatarWidget(data: data), - if (showRestrictedBadge) - Positioned( - bottom: -2.0, - right: -2.0, - child: Tooltip( - message: restrictedBadgeTooltip ?? '', - child: Container( - padding: const EdgeInsets.all(4.0), - decoration: BoxDecoration( - color: theme.colorScheme.surface, - shape: BoxShape.circle, - ), - child: Icon( - Icons.lock, - color: theme.colorScheme.error, - size: 18.0, - semanticLabel: restrictedBadgeSemanticLabel, - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/packages/ui/src/widgets/identity/instance_avatar.dart b/lib/packages/ui/src/widgets/identity/instance_avatar.dart deleted file mode 100644 index 2723ee867..000000000 --- a/lib/packages/ui/src/widgets/identity/instance_avatar.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/packages/ui/src/models/identity/avatar_data.dart'; -import 'package:thunder/packages/ui/src/widgets/identity/avatar_widgets.dart'; - -class InstanceAvatar extends StatelessWidget { - const InstanceAvatar({ - super.key, - required this.data, - }); - - final AvatarData data; - - @override - Widget build(BuildContext context) { - return InstanceAvatarWidget(data: data); - } -} diff --git a/lib/packages/ui/src/widgets/identity/user_avatar.dart b/lib/packages/ui/src/widgets/identity/user_avatar.dart deleted file mode 100644 index 880d8cb81..000000000 --- a/lib/packages/ui/src/widgets/identity/user_avatar.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/packages/ui/src/models/identity/avatar_data.dart'; -import 'package:thunder/packages/ui/src/widgets/identity/avatar_widgets.dart'; - -class UserAvatar extends StatelessWidget { - const UserAvatar({ - super.key, - required this.data, - }); - - final AvatarData data; - - @override - Widget build(BuildContext context) { - return UserAvatarWidget(data: data); - } -} diff --git a/lib/packages/ui/src/widgets/layout/thunder_bottom_sheet.dart b/lib/packages/ui/src/widgets/layout/thunder_bottom_sheet.dart new file mode 100644 index 000000000..721add5d7 --- /dev/null +++ b/lib/packages/ui/src/widgets/layout/thunder_bottom_sheet.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; + +class ThunderBottomSheetHeader extends StatelessWidget { + const ThunderBottomSheetHeader({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.trailing, + }); + + final String title; + final String? subtitle; + final Widget? leading; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (leading != null) ...[ + leading!, + const SizedBox(width: 12), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: theme.textTheme.titleLarge), + if (subtitle != null) ...[ + const SizedBox(height: 2), + Text(subtitle!, style: theme.textTheme.bodySmall), + ], + ], + ), + ), + if (trailing != null) trailing!, + ], + ), + ); + } +} + +class ThunderBottomSheetNavigator extends StatefulWidget { + const ThunderBottomSheetNavigator({ + super.key, + required this.initialPage, + required this.pageBuilder, + required this.titleBuilder, + this.canPop, + this.onClose, + }); + + final T initialPage; + final Widget Function(BuildContext context, T page, void Function(T nextPage) goTo, VoidCallback goBack) pageBuilder; + final String Function(BuildContext context, T page) titleBuilder; + final bool Function(T page)? canPop; + final VoidCallback? onClose; + + @override + State> createState() => _ThunderBottomSheetNavigatorState(); +} + +class _ThunderBottomSheetNavigatorState extends State> { + late final List _history = [widget.initialPage]; + + T get _current => _history.last; + + void _goTo(T page) => setState(() => _history.add(page)); + + void _goBack() { + final allowed = widget.canPop?.call(_current) ?? _history.length > 1; + if (!allowed) return; + + setState(() { + if (_history.length > 1) { + _history.removeLast(); + } else { + Navigator.of(context).pop(); + widget.onClose?.call(); + } + }); + } + + @override + Widget build(BuildContext context) { + final canPop = _history.length > 1; + + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ThunderBottomSheetHeader( + title: widget.titleBuilder(context, _current), + leading: canPop + ? IconButton( + icon: const Icon(Icons.arrow_back_rounded), + onPressed: _goBack, + ) + : null, + trailing: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + Navigator.of(context).pop(); + widget.onClose?.call(); + }, + ), + ), + Flexible( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: widget.pageBuilder(context, _current, _goTo, _goBack), + ), + ), + ], + ), + ); + } +} diff --git a/lib/packages/ui/src/widgets/markdown/markdown_body.dart b/lib/packages/ui/src/widgets/markdown/markdown_body.dart deleted file mode 100644 index b5ed75ff0..000000000 --- a/lib/packages/ui/src/widgets/markdown/markdown_body.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:thunder/packages/ui/src/widgets/markdown/common_markdown_body.dart'; - -typedef MarkdownBody = CommonMarkdownBody; diff --git a/lib/packages/ui/src/widgets/media/compact_thumbnail_preview.dart b/lib/packages/ui/src/widgets/media/compact_thumbnail_preview.dart deleted file mode 100644 index 6e9a88ae4..000000000 --- a/lib/packages/ui/src/widgets/media/compact_thumbnail_preview.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/packages/ui/src/models/content/content_action_handlers.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media_type.dart'; -import 'package:thunder/packages/ui/src/models/content/content_view_mode.dart'; -import 'package:thunder/packages/ui/src/widgets/media/media_view.dart'; - -/// Displays a compact thumbnail preview for a post card. -class CompactThumbnailPreview extends StatelessWidget { - const CompactThumbnailPreview({ - super.key, - required this.media, - this.dim = false, - this.postId, - this.navigateToPost, - this.hideNsfwPreviews = true, - this.markPostReadOnMediaView = false, - this.isUserLoggedIn = false, - this.handlers = const ContentActionHandlers(), - this.nsfwWarningLabel = 'NSFW', - }); - - /// The media to display in the thumbnail. - final ContentMedia media; - - /// Whether or not to dim the thumbnail. - final bool dim; - - /// The post associated with the media. - final int? postId; - - /// The callback function to navigate to the post. - final void Function()? navigateToPost; - - /// Whether to hide NSFW previews. - final bool hideNsfwPreviews; - - /// Whether viewing media marks the post as read. - final bool markPostReadOnMediaView; - - /// Whether the user is currently logged in. - final bool isUserLoggedIn; - - /// Optional action handlers for navigation behavior. - final ContentActionHandlers handlers; - - /// Localized NSFW warning label. - final String nsfwWarningLabel; - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: ExcludeSemantics( - child: Stack( - alignment: AlignmentDirectional.bottomEnd, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), - child: MediaView( - media: media, - postId: postId, - showFullHeightImages: false, - hideNsfwPreviews: hideNsfwPreviews, - markPostReadOnMediaView: markPostReadOnMediaView, - viewMode: ContentViewMode.compact, - isUserLoggedIn: isUserLoggedIn, - navigateToPost: navigateToPost, - read: dim, - handlers: handlers, - nsfwWarningLabel: nsfwWarningLabel, - ), - ), - Padding( - padding: const EdgeInsets.only(right: 6.0), - child: _MediaTypeBadge(mediaType: media.mediaType, dim: dim), - ), - ], - ), - ), - ); - } -} - -class _MediaTypeBadge extends StatelessWidget { - const _MediaTypeBadge({required this.mediaType, required this.dim}); - - final ContentMediaType mediaType; - final bool dim; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final icon = switch (mediaType) { - ContentMediaType.image => Icons.image_outlined, - ContentMediaType.video => Icons.play_arrow_rounded, - ContentMediaType.text => Icons.wysiwyg_rounded, - ContentMediaType.link => Icons.link_rounded, - }; - - final foreground = theme.colorScheme.onSurface.withValues(alpha: dim ? 0.55 : 1); - final background = theme.colorScheme.surface.withValues(alpha: 0.8); - - return Container( - decoration: BoxDecoration( - color: background, - borderRadius: BorderRadius.circular(8.0), - ), - padding: const EdgeInsets.all(4.0), - child: Icon(icon, size: 14.0, color: foreground), - ); - } -} diff --git a/lib/packages/ui/src/widgets/pickers/bottom_sheet_list_picker.dart b/lib/packages/ui/src/widgets/pickers/bottom_sheet_list_picker.dart index 2378bebcb..da78ad037 100644 --- a/lib/packages/ui/src/widgets/pickers/bottom_sheet_list_picker.dart +++ b/lib/packages/ui/src/widgets/pickers/bottom_sheet_list_picker.dart @@ -1,7 +1,6 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:thunder/packages/ui/src/widgets/pickers/picker_item.dart'; -import 'package:thunder/l10n/generated/app_localizations.dart'; class BottomSheetListPicker extends StatefulWidget { final String title; @@ -11,6 +10,7 @@ class BottomSheetListPicker extends StatefulWidget { final bool closeOnSelect; final Widget? heading; final Widget Function()? onUpdateHeading; + final String saveButtonLabel; const BottomSheetListPicker({ super.key, @@ -21,6 +21,7 @@ class BottomSheetListPicker extends StatefulWidget { this.closeOnSelect = true, this.heading, this.onUpdateHeading, + this.saveButtonLabel = 'Save', }); @override @@ -162,7 +163,7 @@ class _BottomSheetListPickerState extends State> { backgroundColor: theme.colorScheme.primaryContainer, ), child: Text( - AppLocalizations.of(context)!.save, + widget.saveButtonLabel, style: TextStyle(color: theme.colorScheme.onPrimaryContainer), ), onPressed: () { diff --git a/lib/packages/ui/src/widgets/pickers/picker_item.dart b/lib/packages/ui/src/widgets/pickers/picker_item.dart index 192312875..421f84401 100644 --- a/lib/packages/ui/src/widgets/pickers/picker_item.dart +++ b/lib/packages/ui/src/widgets/pickers/picker_item.dart @@ -1,69 +1,69 @@ -import 'package:flutter/material.dart'; - -class PickerItem extends StatelessWidget { - final String label; - final String? subtitle; - final Widget? subtitleWidget; - final Widget? labelWidget; - final IconData? icon; - final Widget? leading; - final IconData? trailingIcon; - final void Function()? onSelected; - final bool? isSelected; - final TextTheme? textTheme; - final bool softWrap; - - const PickerItem({ - super.key, - required this.label, - this.subtitle, - this.subtitleWidget, - this.labelWidget, - required this.icon, - required this.onSelected, - this.isSelected, - this.trailingIcon, - this.leading, - this.textTheme, - this.softWrap = false, - }); - - @override - Widget build(BuildContext context) { - ThemeData theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: Material( - borderRadius: BorderRadius.circular(50), - color: isSelected == true ? theme.colorScheme.primaryContainer.withValues(alpha: 0.25) : Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(50), - onTap: onSelected, - child: ListTile( - title: labelWidget ?? - Text( - label, - style: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.copyWith( - color: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.color?.withValues(alpha: onSelected == null ? 0.5 : 1), - ), - textScaler: TextScaler.noScaling, - ), - subtitle: subtitleWidget ?? - (subtitle != null - ? Text( - subtitle!, - style: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.copyWith( - color: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.color?.withValues(alpha: 0.5), - ), - softWrap: softWrap, - overflow: TextOverflow.fade, - ) - : null), - leading: icon != null ? Icon(icon) : leading, - trailing: trailingIcon != null ? Icon(trailingIcon) : null, - ), - ), - ), - ); - } -} +import 'package:flutter/material.dart'; + +class PickerItem extends StatelessWidget { + final String label; + final String? subtitle; + final Widget? subtitleWidget; + final Widget? labelWidget; + final IconData? icon; + final Widget? leading; + final IconData? trailingIcon; + final void Function()? onSelected; + final bool? isSelected; + final TextTheme? textTheme; + final bool softWrap; + + const PickerItem({ + super.key, + required this.label, + this.subtitle, + this.subtitleWidget, + this.labelWidget, + this.icon, + this.onSelected, + this.isSelected, + this.trailingIcon, + this.leading, + this.textTheme, + this.softWrap = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Material( + borderRadius: BorderRadius.circular(50), + color: isSelected == true ? theme.colorScheme.primaryContainer.withValues(alpha: 0.25) : Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: onSelected, + child: ListTile( + title: labelWidget ?? + Text( + label, + style: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.copyWith( + color: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.color?.withValues(alpha: onSelected == null ? 0.5 : 1), + ), + textScaler: TextScaler.noScaling, + ), + subtitle: subtitleWidget ?? + (subtitle != null + ? Text( + subtitle!, + style: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.copyWith( + color: (textTheme?.bodyMedium ?? theme.textTheme.bodyMedium)?.color?.withValues(alpha: 0.5), + ), + softWrap: softWrap, + overflow: TextOverflow.fade, + ) + : null), + leading: icon != null ? Icon(icon) : leading, + trailing: trailingIcon != null ? Icon(trailingIcon) : null, + ), + ), + ), + ); + } +} diff --git a/lib/packages/ui/src/widgets/settings/thunder_expandable_option.dart b/lib/packages/ui/src/widgets/settings/thunder_expandable_option.dart new file mode 100644 index 000000000..a391c1e55 --- /dev/null +++ b/lib/packages/ui/src/widgets/settings/thunder_expandable_option.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +class ThunderExpandableOption extends StatefulWidget { + const ThunderExpandableOption({ + super.key, + this.icon, + required this.title, + required this.child, + this.initiallyExpanded = false, + }); + + final Widget? icon; + final String title; + final Widget child; + final bool initiallyExpanded; + + @override + State createState() => _ThunderExpandableOptionState(); +} + +class _ThunderExpandableOptionState extends State with SingleTickerProviderStateMixin { + late bool _isExpanded; + + late final AnimationController _controller = AnimationController( + duration: const Duration(milliseconds: 120), + vsync: this, + ); + + late final Animation _offsetAnimation = Tween( + begin: Offset.zero, + end: const Offset(1.5, 0), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn)); + + @override + void initState() { + super.initState(); + _isExpanded = widget.initiallyExpanded; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + children: [ + InkWell( + borderRadius: const BorderRadius.all(Radius.circular(50)), + onTap: () => setState(() => _isExpanded = !_isExpanded), + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + if (widget.icon != null) ...[ + widget.icon!, + const SizedBox(width: 8), + ], + Expanded(child: Text(widget.title, style: theme.textTheme.bodyMedium)), + ], + ), + ), + const SizedBox(height: 40), + Icon(_isExpanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded), + ], + ), + ), + ), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + transitionBuilder: (child, animation) { + return SizeTransition( + sizeFactor: animation, + child: SlideTransition(position: _offsetAnimation, child: child), + ); + }, + child: _isExpanded + ? Padding( + padding: const EdgeInsets.all(6), + child: widget.child, + ) + : const SizedBox.shrink(), + ), + ], + ); + } +} diff --git a/lib/packages/ui/src/widgets/settings/thunder_list_option.dart b/lib/packages/ui/src/widgets/settings/thunder_list_option.dart new file mode 100644 index 000000000..1044f05b6 --- /dev/null +++ b/lib/packages/ui/src/widgets/settings/thunder_list_option.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; + +import 'package:flex_color_scheme/flex_color_scheme.dart'; + +import 'package:thunder/packages/ui/src/widgets/pickers/bottom_sheet_list_picker.dart'; +import 'package:thunder/packages/ui/src/widgets/settings/thunder_settings_tile.dart'; + +class ThunderListOption extends StatelessWidget { + const ThunderListOption({ + super.key, + required this.title, + required this.value, + required this.options, + this.subtitle, + this.subtitleWidget, + this.leading, + this.bottomSheetHeading, + this.onChanged, + this.customListPicker, + this.isBottomModalScrollControlled, + this.disabled = false, + this.valueDisplay, + this.closeOnSelect = true, + this.onUpdateHeading, + this.highlighted = false, + this.highlightKey, + this.highlightColor, + this.onLongPress, + this.semanticLabel, + this.saveButtonLabel = 'Save', + }); + + final String title; + final String? subtitle; + final Widget? subtitleWidget; + final Widget? leading; + final Widget? bottomSheetHeading; + final ListPickerItem value; + final List> options; + final Future Function(ListPickerItem)? onChanged; + final Widget? customListPicker; + final bool? isBottomModalScrollControlled; + final bool disabled; + final Widget? valueDisplay; + final bool closeOnSelect; + final Widget Function()? onUpdateHeading; + final bool highlighted; + final GlobalKey? highlightKey; + final Color? highlightColor; + final VoidCallback? onLongPress; + final String? semanticLabel; + final String saveButtonLabel; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final trailing = Row( + mainAxisSize: MainAxisSize.min, + children: [ + valueDisplay ?? + Text( + value.capitalizeLabel + ? value.label.capitalize.replaceAll('_', '').replaceAll(' ', '').replaceAllMapped(RegExp(r'([A-Z])'), (match) { + return ' ${match.group(0)}'; + }) + : value.label, + style: theme.textTheme.titleSmall?.copyWith( + color: disabled ? theme.colorScheme.onSurface.withValues(alpha: 0.5) : theme.colorScheme.onSurface, + ), + ), + Icon( + Icons.chevron_right_rounded, + color: disabled ? theme.colorScheme.onSurface.withValues(alpha: 0.5) : null, + ), + const SizedBox(height: 42), + ], + ); + + return ThunderSettingsTile( + title: title, + subtitle: subtitle, + subtitleWidget: subtitleWidget, + semanticLabel: semanticLabel, + leading: leading, + trailing: trailing, + highlighted: highlighted, + highlightKey: highlightKey, + highlightColor: highlightColor, + enabled: !disabled, + onLongPress: disabled ? null : onLongPress, + onTap: disabled + ? null + : () { + showModalBottomSheet( + context: context, + showDragHandle: true, + isScrollControlled: isBottomModalScrollControlled ?? false, + builder: (context) { + if (customListPicker != null) return customListPicker!; + + return BottomSheetListPicker( + title: title, + heading: bottomSheetHeading, + onUpdateHeading: onUpdateHeading, + items: options, + onSelect: onChanged ?? (_) async {}, + previouslySelected: value.payload, + closeOnSelect: closeOnSelect, + saveButtonLabel: saveButtonLabel, + ); + }, + ); + }, + subtitleMaxLines: subtitleWidget == null ? null : 1, + ); + } +} diff --git a/lib/packages/ui/src/widgets/settings/thunder_settings_tile.dart b/lib/packages/ui/src/widgets/settings/thunder_settings_tile.dart new file mode 100644 index 000000000..ed44c4cb2 --- /dev/null +++ b/lib/packages/ui/src/widgets/settings/thunder_settings_tile.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +import 'package:smooth_highlight/smooth_highlight.dart'; + +class ThunderSettingsTile extends StatelessWidget { + const ThunderSettingsTile({ + super.key, + required this.title, + this.subtitle, + this.subtitleWidget, + this.leading, + this.trailing, + this.onTap, + this.onLongPress, + this.semanticLabel, + this.subtitleMaxLines, + this.padding, + this.highlighted = false, + this.highlightKey, + this.highlightColor, + this.enabled = true, + }); + + final String title; + final String? subtitle; + final Widget? subtitleWidget; + final Widget? leading; + final Widget? trailing; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final String? semanticLabel; + final int? subtitleMaxLines; + final EdgeInsetsGeometry? padding; + final bool highlighted; + final GlobalKey? highlightKey; + final Color? highlightColor; + final bool enabled; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final bool interactive = enabled && (onTap != null || onLongPress != null); + final subtitleStyle = theme.textTheme.bodySmall?.copyWith( + color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.8), + ); + + return SmoothHighlight( + key: highlighted ? highlightKey : null, + useInitialHighLight: highlighted, + enabled: highlighted, + color: highlightColor ?? theme.colorScheme.primaryContainer, + child: Padding( + padding: padding ?? const EdgeInsets.symmetric(horizontal: 16), + child: Semantics( + label: semanticLabel ?? title, + child: InkWell( + borderRadius: const BorderRadius.all(Radius.circular(50)), + onTap: interactive ? onTap : null, + onLongPress: interactive ? onLongPress : null, + child: Padding( + padding: const EdgeInsets.only(left: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row( + children: [ + if (leading != null) ...[ + leading!, + const SizedBox(width: 8), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: interactive + ? theme.textTheme.bodyMedium + : theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5), + ), + ), + if (subtitleWidget != null) subtitleWidget!, + if (subtitle != null) + Text( + subtitle!, + maxLines: subtitleMaxLines, + style: subtitleStyle, + ), + ], + ), + ), + ], + ), + ), + if (trailing != null) trailing!, + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/packages/ui/src/widgets/settings/thunder_toggle_option.dart b/lib/packages/ui/src/widgets/settings/thunder_toggle_option.dart new file mode 100644 index 000000000..f91bd3b9e --- /dev/null +++ b/lib/packages/ui/src/widgets/settings/thunder_toggle_option.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:thunder/packages/ui/src/widgets/settings/thunder_settings_tile.dart'; + +class ThunderToggleOption extends StatelessWidget { + const ThunderToggleOption({ + super.key, + required this.title, + this.subtitle, + this.semanticLabel, + this.value, + this.onChanged, + this.onTap, + this.onLongPress, + this.iconEnabled, + this.iconDisabled, + this.iconEnabledSize, + this.iconDisabledSize, + this.iconSpacing = 8, + this.additionalTrailing = const [], + this.padding, + this.highlighted = false, + this.highlightKey, + this.highlightColor, + this.disabled = false, + }); + + final String title; + final String? subtitle; + final String? semanticLabel; + final bool? value; + final ValueChanged? onChanged; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final IconData? iconEnabled; + final IconData? iconDisabled; + final double? iconEnabledSize; + final double? iconDisabledSize; + final double iconSpacing; + final List additionalTrailing; + final EdgeInsetsGeometry? padding; + final bool highlighted; + final GlobalKey? highlightKey; + final Color? highlightColor; + final bool disabled; + + void _handleTap() { + if (onTap != null) { + onTap!.call(); + return; + } + + if (value != null && onChanged != null) { + onChanged!.call(!value!); + } + } + + @override + Widget build(BuildContext context) { + final leading = (iconEnabled != null && iconDisabled != null) + ? Icon( + value == true ? iconEnabled : iconDisabled, + size: value == true ? iconEnabledSize : iconDisabledSize, + ) + : null; + + final trailing = Row( + mainAxisSize: MainAxisSize.min, + children: [ + ...additionalTrailing, + if (additionalTrailing.isNotEmpty) const SizedBox(width: 12), + if (value != null) + Switch( + value: value!, + onChanged: disabled || onChanged == null + ? null + : (next) { + HapticFeedback.lightImpact(); + onChanged!(next); + }, + ) + else + const SizedBox(height: 50, width: 60), + ], + ); + + return ThunderSettingsTile( + title: title, + subtitle: subtitle, + semanticLabel: semanticLabel, + leading: leading == null + ? null + : Row( + mainAxisSize: MainAxisSize.min, + children: [leading, SizedBox(width: iconSpacing)], + ), + trailing: trailing, + padding: padding, + highlighted: highlighted, + highlightKey: highlightKey, + highlightColor: highlightColor, + enabled: !disabled && (onChanged != null || onTap != null || onLongPress != null), + onTap: disabled ? null : _handleTap, + onLongPress: disabled ? null : onLongPress, + ); + } +} diff --git a/lib/packages/ui/ui.dart b/lib/packages/ui/ui.dart index 1365c5f10..511ca1abe 100644 --- a/lib/packages/ui/ui.dart +++ b/lib/packages/ui/ui.dart @@ -1,34 +1,24 @@ -export 'src/models/content/content_action_handlers.dart'; -export 'src/models/content/content_media.dart'; -export 'src/models/content/content_media_type.dart'; -export 'src/models/content/content_view_mode.dart'; -export 'src/widgets/markdown/common_markdown_body.dart'; -export 'src/widgets/media/image_preview.dart'; -export 'src/widgets/media/image_viewer.dart'; -export 'src/utils/media/media_utils.dart'; -export 'src/widgets/content/content_renderer.dart'; -export 'src/widgets/markdown/markdown_body.dart'; -export 'src/widgets/media/compact_thumbnail_preview.dart'; -export 'src/widgets/media/media_view.dart'; -export 'src/utils/links/link_navigation_utils.dart'; -export 'src/models/identity/avatar_data.dart'; -export 'src/models/identity/identity_name_data.dart'; -export 'src/models/identity/name_style.dart'; -export 'src/utils/identity/name_formatting.dart'; -export 'src/widgets/identity/avatar_widgets.dart'; -export 'src/widgets/identity/community_avatar.dart'; -export 'src/widgets/identity/instance_avatar.dart'; -export 'src/widgets/identity/user_avatar.dart'; -export 'src/widgets/identity/full_name_widgets.dart'; -export 'src/widgets/identity/scalable_text.dart'; -export 'src/widgets/dialogs/thunder_dialog.dart'; -export 'src/widgets/pickers/multi_picker_item.dart'; -export 'src/widgets/pickers/picker_item.dart'; -export 'src/widgets/pickers/bottom_sheet_list_picker.dart'; -export 'src/widgets/actions/bottom_sheet_action.dart'; -export 'src/widgets/layout/conditional_parent_widget.dart'; -export 'src/widgets/layout/thunder_divider.dart'; -export 'src/widgets/feedback/snackbar.dart'; export 'src/icons/thunder_icons.dart'; +export 'src/widgets/avatar/models/avatar_data.dart'; +export 'src/widgets/actions/bottom_sheet_action.dart'; export 'src/widgets/actions/thunder_action_chip.dart'; +export 'src/widgets/actions/thunder_multi_action_dismissible.dart'; export 'src/widgets/actions/thunder_popup_menu_item.dart'; +export 'src/widgets/common/thunder_error_state.dart'; +export 'src/widgets/common/thunder_icon_label.dart'; +export 'src/widgets/dialogs/thunder_dialog.dart'; +export 'src/widgets/dialogs/thunder_typeahead_dialog.dart'; +export 'src/widgets/fab/thunder_expandable_fab.dart'; +export 'src/widgets/feedback/snackbar.dart'; +export 'src/widgets/avatar/avatar.dart'; +export 'src/widgets/identity/scalable_text.dart'; +export 'src/widgets/layout/conditional_parent_widget.dart'; +export 'src/widgets/layout/thunder_bottom_sheet.dart'; +export 'src/widgets/layout/thunder_divider.dart'; +export 'src/widgets/pickers/bottom_sheet_list_picker.dart'; +export 'src/widgets/pickers/multi_picker_item.dart'; +export 'src/widgets/pickers/picker_item.dart'; +export 'src/widgets/settings/thunder_expandable_option.dart'; +export 'src/widgets/settings/thunder_list_option.dart'; +export 'src/widgets/settings/thunder_settings_tile.dart'; +export 'src/widgets/settings/thunder_toggle_option.dart'; diff --git a/lib/src/app/bootstrap/preferences_migration.dart b/lib/src/app/bootstrap/preferences_migration.dart index 6147f11a8..7077c7f2f 100644 --- a/lib/src/app/bootstrap/preferences_migration.dart +++ b/lib/src/app/bootstrap/preferences_migration.dart @@ -9,7 +9,7 @@ import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/drafts/drafts.dart'; import 'package:thunder/src/features/notification/notification.dart'; -import 'package:thunder/packages/ui/ui.dart' show NameColor; +import 'package:thunder/src/shared/identity/models/name_style.dart' show NameColor; /// Performs migrations for shared preferences. Future performSharedPreferencesMigration() async { diff --git a/lib/src/app/shell/navigation/link_navigation_utils.dart b/lib/src/app/shell/navigation/link_navigation_utils.dart index 859898313..63ca0671c 100644 --- a/lib/src/app/shell/navigation/link_navigation_utils.dart +++ b/lib/src/app/shell/navigation/link_navigation_utils.dart @@ -16,7 +16,7 @@ import 'package:thunder/src/features/post/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:thunder/src/app/shell/navigation/loading_page.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_utils.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/api.dart'; diff --git a/lib/src/app/shell/pages/thunder_page.dart b/lib/src/app/shell/pages/thunder_page.dart index 5d827f1b6..d3e9fbd85 100644 --- a/lib/src/app/shell/pages/thunder_page.dart +++ b/lib/src/app/shell/pages/thunder_page.dart @@ -24,7 +24,7 @@ import 'package:thunder/src/app/shell/routing/deep_link.dart'; import 'package:thunder/src/app/share/share_intent_handler.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/app/state/deep_links_cubit/deep_links_cubit.dart'; import 'package:thunder/src/features/notification/application/state/notifications_cubit.dart'; import 'package:thunder/src/features/settings/api.dart'; diff --git a/lib/src/features/comment/presentation/pages/create_comment_page.dart b/lib/src/features/comment/presentation/pages/create_comment_page.dart index 7efbb3803..bc02d70f2 100644 --- a/lib/src/features/comment/presentation/pages/create_comment_page.dart +++ b/lib/src/features/comment/presentation/pages/create_comment_page.dart @@ -18,7 +18,7 @@ import 'package:thunder/src/features/drafts/drafts.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/post/post.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/shared/language_selector.dart'; @@ -27,7 +27,8 @@ import 'package:thunder/src/shared/theme/color_utils.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show selectImagesToUpload, showSnackbar; +import 'package:thunder/packages/ui/ui.dart' show showSnackbar; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart' show selectImagesToUpload; class CreateCommentPage extends StatefulWidget { /// The account to use for composing this comment. diff --git a/lib/src/features/comment/presentation/widgets/comment_card/additional_comment_card.dart b/lib/src/features/comment/presentation/widgets/comment_card/additional_comment_card.dart index b6b534bda..dfc195c99 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/additional_comment_card.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/additional_comment_card.dart @@ -7,7 +7,7 @@ import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/comment/comment.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; class AdditionalCommentCard extends StatefulWidget { /// The function to call when tapped @@ -65,7 +65,7 @@ class _AdditionalCommentCardState extends State { padding: const EdgeInsets.fromLTRB(12.0, 12.0, 0.0, 12.0), child: ScalableText( reply, - fontScale: commentFontSizeScale, + textScaleFactor: commentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5), ), diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card.dart index e6212fcea..8321edefa 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card.dart @@ -9,8 +9,8 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/comment/api.dart'; -import 'package:thunder/src/shared/widgets/multi_action_dismissible.dart'; import 'package:thunder/src/shared/gestures/swipe_utils.dart'; +import 'package:thunder/packages/ui/ui.dart' show ThunderMultiActionDismissible, ThunderSwipeAction; /// A widget that displays a given comment. /// @@ -221,20 +221,20 @@ class _CommentCardState extends State { ); if (currentSwipeDirection != DismissDirection.none) { - child = MultiActionDismissible( + child = ThunderMultiActionDismissible( key: ObjectKey(comment.id), direction: currentSwipeDirection, - leftActions: leftActions, - rightActions: rightActions, + leftActions: leftActions.map((action) => ThunderSwipeAction(value: action, icon: action.getIcon(), color: (context) => action.getColor(context))).toList(), + rightActions: rightActions.map((action) => ThunderSwipeAction(value: action, icon: action.getIcon(), color: (context) => action.getColor(context))).toList(), actionThresholds: actionThresholds, enableBackSwipeOverride: true, onProgressChanged: (progress, _, __) { final dragged = progress > 0; if (dragged != _dragged) setState(() => _dragged = dragged); }, - onAction: (action) => _onAction(action), + onAction: (action) => _onAction(action.value), backgroundBuilder: (context, dismissDirection, progress, action) => CommentCardBackground( - swipeAction: action == SwipeAction.reply && isOwnComment ? SwipeAction.edit : action, + swipeAction: action?.value == SwipeAction.reply && isOwnComment ? SwipeAction.edit : action?.value, dismissThreshold: progress, firstActionThreshold: actionThresholds.first, dismissDirection: dismissDirection, diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart index 4f622c154..376990d4b 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header.dart @@ -8,7 +8,7 @@ import 'package:thunder/src/features/comment/presentation/widgets/comment_card/c import 'package:thunder/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_reply_count.dart'; import 'package:thunder/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/widgets/chips/user_chip.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/settings/api.dart'; diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_date.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_date.dart index 23f3b4e9e..1e7740cd9 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_date.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_date.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; /// A widget that displays the timestamp for a comment, with special styling for recent comments. /// @@ -34,7 +34,7 @@ class CommentCardHeaderDate extends StatelessWidget { final metadataFontSizeScale = context.select((cubit) => cubit.state.metadataFontSizeScale); final formattedDate = ScalableText( date, - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.onSurface), ); diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_reply_count.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_reply_count.dart index 10dcd0093..945e8b8fa 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_reply_count.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_reply_count.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; /// A widget that displays the number of replies to a comment. /// @@ -35,7 +35,7 @@ class CommentCardHeaderReplyCount extends StatelessWidget { color: theme.colorScheme.primaryContainer, borderRadius: const BorderRadius.all(Radius.elliptical(5.0, 5.0)), ), - child: ScalableText('+$replies', fontScale: metadataFontSizeScale), + child: ScalableText('+$replies', textScaleFactor: metadataFontSizeScale.textScaleFactor), ), ); } diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart index 89a152916..08c7f6f89 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_card_header/comment_card_header_score.dart @@ -8,7 +8,7 @@ import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; /// A widget that displays voting scores for comments with upvote/downvote indicators /// @@ -67,7 +67,7 @@ class CommentCardHeaderScore extends StatelessWidget { ScalableText( scoreLabel, semanticsLabel: l10n.xScore(scoreLabel), - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: (voteType != null && voteType != 0) ? voteType == 1 @@ -90,7 +90,7 @@ class CommentCardHeaderScore extends StatelessWidget { ScalableText( upvotesLabel, semanticsLabel: l10n.xUpvotes(upvotesLabel), - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: (voteType == 1) ? upvoteColor : theme.colorScheme.onSurface, ), @@ -102,7 +102,7 @@ class CommentCardHeaderScore extends StatelessWidget { ScalableText( downvotesLabel, semanticsLabel: l10n.xDownvotes(downvotesLabel), - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: (voteType == -1) ? downvoteColor : theme.colorScheme.onSurface, ), diff --git a/lib/src/features/comment/presentation/widgets/comment_card/comment_content.dart b/lib/src/features/comment/presentation/widgets/comment_card/comment_content.dart index 8c3d89245..4dc3970ab 100644 --- a/lib/src/features/comment/presentation/widgets/comment_card/comment_content.dart +++ b/lib/src/features/comment/presentation/widgets/comment_card/comment_content.dart @@ -8,9 +8,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/comment/comment.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/shared/reply_to_preview_actions.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/packages/ui/ui.dart' show ConditionalParentWidget; @@ -137,7 +137,7 @@ class _CommentContentState extends State with SingleTickerProvid ? ScalableText( content, style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, ) : CommonMarkdownBody(body: content, isComment: true), ), diff --git a/lib/src/features/comment/presentation/widgets/comment_reference.dart b/lib/src/features/comment/presentation/widgets/comment_reference.dart index 32d16eb88..f0401843e 100644 --- a/lib/src/features/comment/presentation/widgets/comment_reference.dart +++ b/lib/src/features/comment/presentation/widgets/comment_reference.dart @@ -6,9 +6,9 @@ import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/api.dart'; import 'package:thunder/src/features/comment/api.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; @@ -131,7 +131,7 @@ class _CommentReferenceHeader extends StatelessWidget { ExcludeSemantics( child: ScalableText( l10n.in_, - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.4), ), @@ -139,13 +139,11 @@ class _CommentReferenceHeader extends StatelessWidget { ), ExcludeSemantics( child: CommunityFullNameWidget( - context, - comment.community?.name, - comment.community?.title, - fetchInstanceNameFromUrl(comment.community?.actorId), - fontScale: contentFontSizeScale, - transformColor: (color) => color?.withValues(alpha: 0.75), - ), + name: comment.community?.name, + displayName: comment.community?.title, + instance: fetchInstanceNameFromUrl(comment.community?.actorId), + fontScale: contentFontSizeScale, + transformColor: (color) => color?.withValues(alpha: 0.75)), ), ], ), diff --git a/lib/src/features/community/presentation/widgets/community_drawer.dart b/lib/src/features/community/presentation/widgets/community_drawer.dart index a9b0516d5..732f0326b 100644 --- a/lib/src/features/community/presentation/widgets/community_drawer.dart +++ b/lib/src/features/community/presentation/widgets/community_drawer.dart @@ -11,8 +11,8 @@ import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/feed.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; diff --git a/lib/src/features/community/presentation/widgets/community_header/community_header.dart b/lib/src/features/community/presentation/widgets/community_header/community_header.dart index c61bc02f9..304761529 100644 --- a/lib/src/features/community/presentation/widgets/community_header/community_header.dart +++ b/lib/src/features/community/presentation/widgets/community_header/community_header.dart @@ -2,12 +2,12 @@ import 'package:flutter/material.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; -import 'package:thunder/src/shared/icon_text.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show ImagePreview; +import 'package:thunder/src/shared/content/widgets/media/image_preview.dart'; +import 'package:thunder/packages/ui/ui.dart' show ThunderIconLabel; /// A widget that displays a community's header information and related actions. /// @@ -123,10 +123,9 @@ class _CommunityInfo extends StatelessWidget { style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w600), ), CommunityFullNameWidget( - context, - community.name, - community.title, - fetchInstanceNameFromUrl(community.actorId), + name: community.name, + displayName: community.title, + instance: fetchInstanceNameFromUrl(community.actorId), useDisplayName: false, // Override because we're showing title above ), const SizedBox(height: 8.0), @@ -150,13 +149,13 @@ class _CommunityStats extends StatelessWidget { return Row( spacing: 10.0, children: [ - IconText( + ThunderIconLabel( icon: Icon(Icons.people_rounded, size: iconSize), - text: formatNumberToK(community.subscribers ?? 0), + label: formatNumberToK(community.subscribers ?? 0), ), - IconText( + ThunderIconLabel( icon: Icon(Icons.library_books_rounded, size: iconSize), - text: formatNumberToK(community.posts ?? 0), + label: formatNumberToK(community.posts ?? 0), ), ], ); diff --git a/lib/src/features/community/presentation/widgets/community_information.dart b/lib/src/features/community/presentation/widgets/community_information.dart index 0e821b92f..ed2233073 100644 --- a/lib/src/features/community/presentation/widgets/community_information.dart +++ b/lib/src/features/community/presentation/widgets/community_information.dart @@ -8,9 +8,9 @@ import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; @@ -165,10 +165,9 @@ class CommunityModeratorList extends StatelessWidget { ), ), UserFullNameWidget( - context, - moderator.name, - moderator.displayName, - fetchInstanceNameFromUrl(moderator.actorId), + name: moderator.name, + displayName: moderator.displayName, + instance: fetchInstanceNameFromUrl(moderator.actorId), textStyle: const TextStyle(fontSize: 13.0), transformColor: (color) => color?.withValues(alpha: 0.6), useDisplayName: false, // Override because we're showing display name above diff --git a/lib/src/features/community/presentation/widgets/community_list_entry.dart b/lib/src/features/community/presentation/widgets/community_list_entry.dart index 23e6ca54b..289368787 100644 --- a/lib/src/features/community/presentation/widgets/community_list_entry.dart +++ b/lib/src/features/community/presentation/widgets/community_list_entry.dart @@ -9,8 +9,8 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/search/search.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; @@ -99,10 +99,9 @@ class _CommunityListEntryState extends State { children: [ Flexible( child: CommunityFullNameWidget( - context, - widget.community.name, - widget.community.title, - fetchInstanceNameFromUrl(widget.community.actorId), + name: widget.community.name, + displayName: widget.community.title, + instance: fetchInstanceNameFromUrl(widget.community.actorId), // Override because we're showing display name above useDisplayName: false, ), diff --git a/lib/src/features/community/presentation/widgets/post_card.dart b/lib/src/features/community/presentation/widgets/post_card.dart index 963bde923..588732c8a 100644 --- a/lib/src/features/community/presentation/widgets/post_card.dart +++ b/lib/src/features/community/presentation/widgets/post_card.dart @@ -13,10 +13,9 @@ import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:thunder/src/features/user/user.dart'; -import 'package:thunder/src/shared/widgets/multi_action_dismissible.dart'; import 'package:thunder/src/shared/gestures/swipe_utils.dart'; import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/packages/ui/ui.dart' show showSnackbar; +import 'package:thunder/packages/ui/ui.dart' show ThunderMultiActionDismissible, ThunderSwipeAction, showSnackbar; class PostCard extends StatefulWidget { /// The associated post information to display in the card. @@ -259,17 +258,17 @@ class _PostCardState extends State { final leftActions = [leftPrimaryPostGesture, leftSecondaryPostGesture].where((action) => action != SwipeAction.none).toList(); final rightActions = [rightPrimaryPostGesture, rightSecondaryPostGesture].where((action) => action != SwipeAction.none).toList(); - child = MultiActionDismissible( + child = ThunderMultiActionDismissible( key: ObjectKey(widget.post.id), direction: widget.disableSwiping ? DismissDirection.none : currentSwipeDirection, - leftActions: leftActions, - rightActions: rightActions, + leftActions: leftActions.map((action) => ThunderSwipeAction(value: action, icon: action.getIcon(read: read, hidden: hidden), color: (context) => action.getColor(context))).toList(), + rightActions: rightActions.map((action) => ThunderSwipeAction(value: action, icon: action.getIcon(read: read, hidden: hidden), color: (context) => action.getColor(context))).toList(), actionThresholds: actionThresholds, - onAction: (action) => _onAction(action), + onAction: (action) => _onAction(action.value), onPointerDown: widget.onDownAction, onDragEnd: (dy) => widget.onUpAction(dy), backgroundBuilder: (context, dir, progress, action) => PostCardActionBackground( - swipeAction: action, + swipeAction: action?.value, dismissThreshold: progress, firstActionThreshold: actionThresholds.first, dismissDirection: dir, diff --git a/lib/src/features/community/presentation/widgets/post_card_metadata.dart b/lib/src/features/community/presentation/widgets/post_card_metadata.dart index 64d1f72e1..97c389a72 100644 --- a/lib/src/features/community/presentation/widgets/post_card_metadata.dart +++ b/lib/src/features/community/presentation/widgets/post_card_metadata.dart @@ -8,10 +8,9 @@ import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/feed/feed.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; -import 'package:thunder/src/shared/icon_text.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText, ThunderIconLabel; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; @@ -210,7 +209,7 @@ class ScorePostCardMetaData extends StatelessWidget { ScalableText( formattedScore, semanticsLabel: l10n.xScore(formattedScore), - fontScale: metadataFontScale, + textScaleFactor: metadataFontScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith(color: primaryColor), ), ], @@ -260,12 +259,12 @@ class UpvotePostCardMetaData extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), - child: IconText( - fontScale: metadataFontScale, - text: formattedUpvotes, - textColor: color, - padding: 2.0, + child: ThunderIconLabel( icon: Icon(Icons.arrow_upward, size: 17.0, color: color), + label: formattedUpvotes, + labelStyle: TextStyle(color: color), + textScaleFactor: metadataFontScale.textScaleFactor, + gap: 2.0, ), ); } @@ -312,12 +311,12 @@ class DownvotePostCardMetaData extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), - child: IconText( - fontScale: metadataFontScale, - text: formattedDownvotes, - textColor: color, - padding: 2.0, + child: ThunderIconLabel( icon: Icon(Icons.arrow_downward, size: 17.0, color: color), + label: formattedDownvotes, + labelStyle: TextStyle(color: color), + textScaleFactor: metadataFontScale.textScaleFactor, + gap: 2.0, ), ); } @@ -359,12 +358,12 @@ class CommentCountPostCardMetaData extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), - child: IconText( - fontScale: fontScale, - text: commentCountText, - textColor: color, - padding: 4.0, + child: ThunderIconLabel( icon: Icon(icon, size: 17.0, color: color), + label: commentCountText, + labelStyle: TextStyle(color: color), + textScaleFactor: fontScale.textScaleFactor, + gap: 4.0, ), ); } @@ -407,12 +406,12 @@ class DateTimePostCardMetaData extends StatelessWidget { return Padding( padding: const EdgeInsets.only(right: 8.0), - child: IconText( - fontScale: fontScale, - text: formattedDate, - textColor: color, - padding: 2.0, + child: ThunderIconLabel( icon: Icon(icon, size: 17.0, color: color), + label: formattedDate, + labelStyle: TextStyle(color: color), + textScaleFactor: fontScale.textScaleFactor, + gap: 2.0, ), ); } @@ -454,12 +453,12 @@ class UrlPostCardMetaData extends StatelessWidget { child: Tooltip( message: url!, preferBelow: false, - child: IconText( - fontScale: fontScale, - text: host ?? url!, - textColor: textColor, - padding: 3.0, + child: ThunderIconLabel( icon: Icon(Icons.public, size: 17.0, color: textColor), + label: host ?? url!, + labelStyle: TextStyle(color: textColor), + textScaleFactor: fontScale.textScaleFactor, + gap: 3.0, ), ), ); @@ -506,12 +505,12 @@ class LanguagePostCardMetaData extends StatelessWidget { child: Tooltip( message: languageName, preferBelow: false, - child: IconText( - fontScale: fontScale, - text: languageName, - textColor: color, - padding: 3.0, + child: ThunderIconLabel( icon: Icon(Icons.map_rounded, size: 17.0, color: color), + label: languageName, + labelStyle: TextStyle(color: color), + textScaleFactor: fontScale.textScaleFactor, + gap: 3.0, ), ), ); @@ -646,14 +645,7 @@ class CommunityPostCardMetadata extends StatelessWidget { final instanceName = actorId != null ? fetchInstanceNameFromUrl(actorId) : null; final showCommunitySubscription = (feedListType == FeedListType.all || feedListType == FeedListType.local) && subscribed; - Widget child = CommunityFullNameWidget( - context, - communityName, - displayName, - instanceName, - fontScale: fontScale, - transformColor: _transformColor, - ); + Widget child = CommunityFullNameWidget(name: communityName, displayName: displayName, instance: instanceName, fontScale: fontScale, transformColor: _transformColor); if (!showCommunitySubscription) return child; @@ -698,13 +690,6 @@ class UserPostCardMetadata extends StatelessWidget { final instanceName = actorId != null ? fetchInstanceNameFromUrl(actorId) : null; return UserFullNameWidget( - context, - username, - displayName, - instanceName, - includeInstance: postShowUserInstance, - fontScale: metadataFontSizeScale, - transformColor: _transformColor, - ); + name: username, displayName: displayName, instance: instanceName, includeInstance: postShowUserInstance, fontScale: metadataFontSizeScale, transformColor: _transformColor); } } diff --git a/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart b/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart index c91fdd801..d2e52d095 100644 --- a/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart +++ b/lib/src/features/community/presentation/widgets/post_card_view_comfortable.dart @@ -9,8 +9,8 @@ import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/feed.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_view.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/src/shared/content/widgets/media/media_view.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -212,7 +212,7 @@ class PostCardViewComfortable extends StatelessWidget { post.textPreview ?? textContent, maxLines: 4, overflow: TextOverflow.ellipsis, - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: post.read == true ? readColor : theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.70), ), diff --git a/lib/src/features/community/presentation/widgets/post_card_view_compact.dart b/lib/src/features/community/presentation/widgets/post_card_view_compact.dart index 3521f0f62..fa01dd259 100644 --- a/lib/src/features/community/presentation/widgets/post_card_view_compact.dart +++ b/lib/src/features/community/presentation/widgets/post_card_view_compact.dart @@ -6,7 +6,7 @@ import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/post/post.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/compact_thumbnail_preview.dart'; +import 'package:thunder/src/shared/content/widgets/media/compact_thumbnail_preview.dart'; import 'package:thunder/src/features/feed/api.dart'; /// Displays a compact view of a post card. This view is used in the feed related pages. diff --git a/lib/src/features/content/presentation/widgets/common_markdown_body.dart b/lib/src/features/content/presentation/widgets/common_markdown_body.dart deleted file mode 100644 index ff6eb9e1f..000000000 --- a/lib/src/features/content/presentation/widgets/common_markdown_body.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/packages/ui/ui.dart' as content; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; -import 'package:thunder/src/shared/links/widgets/link_bottom_sheet.dart'; - -/// App adapter for package-generic markdown renderer. -class CommonMarkdownBody extends StatelessWidget { - /// The markdown content body. - final String body; - - /// Whether to hide the markdown content. - final bool hidden; - - /// Whether the markdown content is NSFW. - final bool nsfw; - - /// Indicates if the given markdown is a comment. - final bool? isComment; - - /// The maximum width of the image. - final double? imageMaxWidth; - - /// Optional action handlers that decouple media and navigation behavior. - final content.ContentActionHandlers handlers; - - const CommonMarkdownBody({ - super.key, - required this.body, - this.hidden = false, - this.nsfw = false, - this.isComment, - this.imageMaxWidth, - this.handlers = const content.ContentActionHandlers(), - }); - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - final commentFontSizeScale = context.select( - (cubit) => cubit.state.commentFontSizeScale, - ); - final contentFontSizeScale = context.select( - (cubit) => cubit.state.contentFontSizeScale, - ); - - final effectiveHandlers = content.ContentActionHandlers( - onOpenLink: handlers.onOpenLink ?? - (context, url) { - handleLink(context, url: url); - }, - onLongPressLink: handlers.onLongPressLink ?? - (context, text, url) { - if (url != null) { - handleLinkLongPress(context, text, url); - } - }, - onOpenImage: handlers.onOpenImage, - onOpenVideo: handlers.onOpenVideo ?? - (context, url) { - handleVideoLink(context, url: url); - }, - onMarkRead: handlers.onMarkRead, - ); - - return content.CommonMarkdownBody( - body: body, - hidden: hidden, - nsfw: nsfw, - isComment: isComment, - imageMaxWidth: imageMaxWidth, - handlers: effectiveHandlers, - commentTextScaleFactor: commentFontSizeScale.textScaleFactor, - contentTextScaleFactor: contentFontSizeScale.textScaleFactor, - retryTooltip: l10n.retry, - nsfwWarningLabel: l10n.nsfwWarning, - ); - } -} diff --git a/lib/src/features/content/presentation/widgets/media/media_utils.dart b/lib/src/features/content/presentation/widgets/media/media_utils.dart deleted file mode 100644 index b6511b7a5..000000000 --- a/lib/src/features/content/presentation/widgets/media/media_utils.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; - -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:thunder/packages/ui/ui.dart' as content; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; - -export 'package:thunder/packages/ui/ui.dart' - show - fetchProxyImageUrl, - getScaledMediaSize, - isImageProxyUrl, - isImageUriSvg, - isImageUrl, - isImageUrlSvg, - isVideoUrl, - processAvifImage, - processImage, - processImageDimensions, - retrieveImageDimensions, - selectImagesToUpload, - showVideoPlayer; - -/// App adapter for content package image viewer opening. -void showImageViewer( - BuildContext context, { - String? url, - Uint8List? bytes, - int? postId, - void Function()? navigateToPost, - String? altText, -}) { - final clearMemoryCacheWhenDispose = context.read().state.imageCachingMode == ImageCachingMode.relaxed; - - content.showImageViewer( - context, - url: url, - bytes: bytes, - postId: postId, - navigateToPost: navigateToPost, - altText: altText, - clearMemoryCacheWhenDispose: clearMemoryCacheWhenDispose, - ); -} diff --git a/lib/src/features/content/presentation/widgets/media/media_view.dart b/lib/src/features/content/presentation/widgets/media/media_view.dart deleted file mode 100644 index 06b072b21..000000000 --- a/lib/src/features/content/presentation/widgets/media/media_view.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/packages/ui/ui.dart' as content; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/post/api.dart'; -import 'package:thunder/src/features/feed/api.dart'; -import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; -import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; -import 'package:thunder/src/shared/links/widgets/link_bottom_sheet.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_utils.dart'; - -/// App adapter for package-generic content media view. -class MediaView extends StatelessWidget { - const MediaView({ - super.key, - required this.media, - this.postId, - this.showFullHeightImages = true, - this.allowUnconstrainedImageHeight = false, - this.edgeToEdgeImages = false, - this.hideNsfwPreviews = true, - this.hideThumbnails = false, - this.markPostReadOnMediaView = false, - this.isUserLoggedIn = false, - this.viewMode = ViewMode.comfortable, - this.navigateToPost, - this.read, - this.handlers = const content.ContentActionHandlers(), - }); - - /// The media information. - final Media media; - - /// The associated post ID for the media. - final int? postId; - - /// Whether to show the full height for images. - final bool showFullHeightImages; - - /// When enabled, the image height will be unconstrained. - final bool allowUnconstrainedImageHeight; - - /// Whether to blur NSFW images. - final bool hideNsfwPreviews; - - /// Whether to hide thumbnails. - final bool hideThumbnails; - - /// Whether to extend the image to the edge of the screen. - final bool edgeToEdgeImages; - - /// Whether to mark the post as read when the media is viewed. - final bool markPostReadOnMediaView; - - /// Whether the user is logged in. - final bool isUserLoggedIn; - - /// The view mode of the media. - final ViewMode viewMode; - - /// The function to navigate to the post. - final void Function()? navigateToPost; - - /// Whether the post has been read. - final bool? read; - - /// Optional action handlers that decouple media and navigation behavior. - final content.ContentActionHandlers handlers; - - @override - Widget build(BuildContext context) { - final imagePeekDurationMs = context.select( - (cubit) => cubit.state.imagePeekDuration, - ); - final tabletMode = viewMode == ViewMode.comfortable ? context.select((ThunderBloc bloc) => bloc.state.tabletMode) : false; - final l10n = AppLocalizations.of(context)!; - - final effectiveHandlers = content.ContentActionHandlers( - onOpenLink: handlers.onOpenLink ?? - (context, url) { - handleLink(context, url: url); - }, - onLongPressLink: handlers.onLongPressLink ?? - (context, text, url) { - if (url != null) { - handleLinkLongPress(context, text, url); - } - }, - onOpenImage: handlers.onOpenImage ?? - (context, {url, bytes}) { - showImageViewer( - context, - url: url, - bytes: bytes, - postId: postId, - navigateToPost: navigateToPost, - altText: media.altText, - ); - }, - onOpenVideo: handlers.onOpenVideo ?? - (context, url) { - handleVideoLink(context, url: url); - }, - onMarkRead: handlers.onMarkRead ?? - (postId) { - try { - final feedBloc = BlocProvider.of(context); - feedBloc.add( - FeedItemActionedEvent( - postAction: PostAction.read, - postId: postId, - actionInput: const ReadPostInput(true), - ), - ); - } catch (e) { - debugPrint('Error marking post as read: $e'); - } - }, - ); - - return content.MediaView( - media: _mapMedia(media), - postId: postId, - showFullHeightImages: showFullHeightImages, - allowUnconstrainedImageHeight: allowUnconstrainedImageHeight, - edgeToEdgeImages: edgeToEdgeImages, - hideNsfwPreviews: hideNsfwPreviews, - hideThumbnails: hideThumbnails, - markPostReadOnMediaView: markPostReadOnMediaView, - isUserLoggedIn: isUserLoggedIn, - viewMode: _mapViewMode(viewMode), - navigateToPost: navigateToPost, - read: read, - handlers: effectiveHandlers, - imagePeekDurationMs: imagePeekDurationMs, - tabletMode: tabletMode, - nsfwWarningLabel: l10n.nsfwWarning, - retryTooltip: l10n.retry, - ); - } -} - -content.ContentMedia _mapMedia(Media media) { - return content.ContentMedia( - thumbnailUrl: media.thumbnailUrl, - mediaUrl: media.mediaUrl, - originalUrl: media.originalUrl, - width: media.width, - height: media.height, - nsfw: media.nsfw, - mediaType: _mapMediaType(media.mediaType), - altText: media.altText, - contentType: media.contentType, - ); -} - -content.ContentViewMode _mapViewMode(ViewMode viewMode) { - return switch (viewMode) { - ViewMode.comment => content.ContentViewMode.comment, - ViewMode.compact => content.ContentViewMode.compact, - ViewMode.comfortable => content.ContentViewMode.comfortable, - }; -} - -content.ContentMediaType _mapMediaType(MediaType mediaType) { - return switch (mediaType) { - MediaType.image => content.ContentMediaType.image, - MediaType.video => content.ContentMediaType.video, - MediaType.link => content.ContentMediaType.link, - MediaType.text => content.ContentMediaType.text, - }; -} diff --git a/lib/src/features/feed/presentation/pages/feed_page.dart b/lib/src/features/feed/presentation/pages/feed_page.dart index 16fa6d260..45affe860 100644 --- a/lib/src/features/feed/presentation/pages/feed_page.dart +++ b/lib/src/features/feed/presentation/pages/feed_page.dart @@ -14,7 +14,7 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/settings/api.dart'; @@ -634,7 +634,7 @@ class FeedReachedEnd extends StatelessWidget { l10n.reachedTheBottom, textAlign: TextAlign.center, style: theme.textTheme.titleSmall, - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, ), ), const SizedBox(height: 160) diff --git a/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart b/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart index 576d9f3d9..679e2377d 100644 --- a/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart +++ b/lib/src/features/feed/presentation/widgets/feed_page_app_bar.dart @@ -12,7 +12,7 @@ import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/sort_picker.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/packages/ui/ui.dart' show ThunderPopupMenuItem; diff --git a/lib/src/features/feed/presentation/widgets/tagline.dart b/lib/src/features/feed/presentation/widgets/tagline.dart index 210918d29..5661e371a 100644 --- a/lib/src/features/feed/presentation/widgets/tagline.dart +++ b/lib/src/features/feed/presentation/widgets/tagline.dart @@ -7,7 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/shared/theme/color_utils.dart'; diff --git a/lib/src/features/identity/presentation/widgets/avatars/community_avatar.dart b/lib/src/features/identity/presentation/widgets/avatars/community_avatar.dart deleted file mode 100644 index fecc39bb3..000000000 --- a/lib/src/features/identity/presentation/widgets/avatars/community_avatar.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/packages/ui/ui.dart' as identity; -import 'package:thunder/src/features/community/api.dart'; - -/// App adapter for the generic identity package avatar. -class CommunityAvatar extends StatelessWidget { - final ThunderCommunity community; - final double radius; - final bool showCommunityStatus; - final int? thumbnailSize; - final String? format; - - const CommunityAvatar({ - super.key, - required this.community, - this.radius = 12.0, - this.showCommunityStatus = false, - this.thumbnailSize, - this.format, - }); - - @override - Widget build(BuildContext context) { - final l10n = AppLocalizations.of(context)!; - - final queryParameters = {}; - if (thumbnailSize != null) { - queryParameters['thumbnail'] = thumbnailSize.toString(); - } - if (format != null) { - queryParameters['format'] = format; - } - - Uri? imageUri = community.icon != null ? Uri.parse(community.icon!) : null; - - if (imageUri != null && imageUri.path.contains('/pictrs/image/') && queryParameters.isNotEmpty) { - imageUri = Uri.https(imageUri.host, imageUri.path, queryParameters); - } - - return identity.CommunityAvatar( - data: identity.AvatarData( - fallbackLabel: community.titleOrName, - imageUrl: imageUri?.toString(), - radius: radius, - ), - showRestrictedBadge: community.postingRestrictedToMods && showCommunityStatus, - restrictedBadgeTooltip: l10n.onlyModsCanPostInCommunity, - restrictedBadgeSemanticLabel: l10n.onlyModsCanPostInCommunity, - ); - } -} diff --git a/lib/src/features/identity/presentation/widgets/avatars/user_avatar.dart b/lib/src/features/identity/presentation/widgets/avatars/user_avatar.dart deleted file mode 100644 index 53a166568..000000000 --- a/lib/src/features/identity/presentation/widgets/avatars/user_avatar.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/packages/ui/ui.dart' as identity; -import 'package:thunder/src/features/user/api.dart'; - -/// App adapter for the generic identity package avatar. -class UserAvatar extends StatelessWidget { - final ThunderUser user; - final double radius; - final int? thumbnailSize; - final String? format; - - const UserAvatar({ - super.key, - required this.user, - this.radius = 16.0, - this.thumbnailSize, - this.format, - }); - - @override - Widget build(BuildContext context) { - final queryParameters = {}; - if (thumbnailSize != null) { - queryParameters['thumbnail'] = thumbnailSize.toString(); - } - if (format != null) { - queryParameters['format'] = format; - } - - Uri? imageUri = user.avatar != null ? Uri.parse(user.avatar!) : null; - - if (imageUri != null && imageUri.path.contains('/pictrs/image/') && queryParameters.isNotEmpty) { - imageUri = Uri.https(imageUri.host, imageUri.path, queryParameters); - } - - return identity.UserAvatar( - data: identity.AvatarData( - fallbackLabel: user.displayNameOrName, - imageUrl: imageUri?.toString(), - radius: radius, - ), - ); - } -} diff --git a/lib/src/features/identity/presentation/widgets/full_name_widgets.dart b/lib/src/features/identity/presentation/widgets/full_name_widgets.dart deleted file mode 100644 index 231578d0f..000000000 --- a/lib/src/features/identity/presentation/widgets/full_name_widgets.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'package:thunder/packages/ui/ui.dart' as identity; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/settings/api.dart'; - -/// App adapter for package-generic full-name widgets. -class UserFullNameWidget extends StatelessWidget { - const UserFullNameWidget( - this.outerContext, - this.name, - this.displayName, - this.instance, { - super.key, - this.userSeparator, - this.userNameThickness, - this.userNameColor, - this.instanceNameThickness, - this.instanceNameColor, - this.textStyle, - this.includeInstance = true, - this.fontScale, - this.autoSize = false, - this.transformColor, - this.useDisplayName, - }) : assert(outerContext != null || - (userSeparator != null && userNameThickness != null && userNameColor != null && instanceNameThickness != null && instanceNameColor != null && useDisplayName != null)), - assert(outerContext != null || textStyle != null); - - final BuildContext? outerContext; - final String? name; - final String? displayName; - final String? instance; - final FullNameSeparator? userSeparator; - final NameThickness? userNameThickness; - final NameColor? userNameColor; - final NameThickness? instanceNameThickness; - final NameColor? instanceNameColor; - final TextStyle? textStyle; - final bool includeInstance; - final FontScale? fontScale; - final bool autoSize; - final Color? Function(Color?)? transformColor; - final bool? useDisplayName; - - @override - Widget build(BuildContext context) { - final lookupContext = outerContext ?? context; - final themePreferences = lookupContext.read().state; - - return identity.UserFullNameWidget( - name: name, - displayName: displayName, - instance: instance, - separator: userSeparator ?? themePreferences.userSeparator, - useDisplayName: useDisplayName ?? themePreferences.useDisplayNamesForUsers, - userNameThickness: userNameThickness ?? themePreferences.userFullNameUserNameThickness, - userNameColor: userNameColor ?? themePreferences.userFullNameUserNameColor, - instanceNameThickness: instanceNameThickness ?? themePreferences.userFullNameInstanceNameThickness, - instanceNameColor: instanceNameColor ?? themePreferences.userFullNameInstanceNameColor, - textStyle: textStyle ?? Theme.of(lookupContext).textTheme.bodyMedium, - includeInstance: includeInstance, - textScaleFactor: fontScale?.textScaleFactor ?? FontScale.base.textScaleFactor, - autoSize: autoSize, - transformColor: transformColor, - ); - } -} - -/// App adapter for package-generic full-name widgets. -class CommunityFullNameWidget extends StatelessWidget { - const CommunityFullNameWidget( - this.outerContext, - this.name, - this.displayName, - this.instance, { - super.key, - this.communitySeparator, - this.communityNameThickness, - this.communityNameColor, - this.instanceNameThickness, - this.instanceNameColor, - this.textStyle, - this.includeInstance = true, - this.fontScale, - this.autoSize = false, - this.transformColor, - this.useDisplayName, - }) : assert(outerContext != null || - (communitySeparator != null && communityNameThickness != null && communityNameColor != null && instanceNameThickness != null && instanceNameColor != null && useDisplayName != null)), - assert(outerContext != null || textStyle != null); - - final BuildContext? outerContext; - final String? name; - final String? displayName; - final String? instance; - final FullNameSeparator? communitySeparator; - final NameThickness? communityNameThickness; - final NameColor? communityNameColor; - final NameThickness? instanceNameThickness; - final NameColor? instanceNameColor; - final TextStyle? textStyle; - final bool includeInstance; - final FontScale? fontScale; - final bool autoSize; - final Color? Function(Color?)? transformColor; - final bool? useDisplayName; - - @override - Widget build(BuildContext context) { - final lookupContext = outerContext ?? context; - final themePreferences = lookupContext.read().state; - - return identity.CommunityFullNameWidget( - name: name, - displayName: displayName, - instance: instance, - separator: communitySeparator ?? themePreferences.communitySeparator, - useDisplayName: useDisplayName ?? themePreferences.useDisplayNamesForCommunities, - communityNameThickness: communityNameThickness ?? themePreferences.communityFullNameCommunityNameThickness, - communityNameColor: communityNameColor ?? themePreferences.communityFullNameCommunityNameColor, - instanceNameThickness: instanceNameThickness ?? themePreferences.communityFullNameInstanceNameThickness, - instanceNameColor: instanceNameColor ?? themePreferences.communityFullNameInstanceNameColor, - textStyle: textStyle ?? Theme.of(lookupContext).textTheme.bodyMedium, - includeInstance: includeInstance, - textScaleFactor: fontScale?.textScaleFactor ?? FontScale.base.textScaleFactor, - autoSize: autoSize, - transformColor: transformColor, - ); - } -} diff --git a/lib/src/features/identity/presentation/widgets/text/scalable_text.dart b/lib/src/features/identity/presentation/widgets/text/scalable_text.dart deleted file mode 100644 index c516cce8e..000000000 --- a/lib/src/features/identity/presentation/widgets/text/scalable_text.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/packages/ui/ui.dart' as identity; -import 'package:thunder/src/foundation/primitives/primitives.dart'; - -/// App adapter for the generic identity package scalable text. -class ScalableText extends StatelessWidget { - final String text; - final TextStyle? style; - final TextAlign? textAlign; - final FontScale? fontScale; - final String? semanticsLabel; - final TextOverflow? overflow; - final int? maxLines; - - const ScalableText( - this.text, { - super.key, - this.style, - this.textAlign, - this.fontScale, - this.semanticsLabel, - this.overflow, - this.maxLines, - }); - - @override - Widget build(BuildContext context) { - return identity.ScalableText( - text, - style: style, - textAlign: textAlign, - textScaleFactor: fontScale?.textScaleFactor ?? FontScale.base.textScaleFactor, - semanticsLabel: semanticsLabel, - overflow: overflow, - maxLines: maxLines, - ); - } -} diff --git a/lib/src/features/inbox/presentation/widgets/inbox_private_messages_view.dart b/lib/src/features/inbox/presentation/widgets/inbox_private_messages_view.dart index 1322ff9e1..83395b381 100644 --- a/lib/src/features/inbox/presentation/widgets/inbox_private_messages_view.dart +++ b/lib/src/features/inbox/presentation/widgets/inbox_private_messages_view.dart @@ -7,8 +7,8 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/inbox/inbox.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show ThunderDivider; @@ -63,23 +63,19 @@ class _InboxPrivateMessagesViewState extends State { crossAxisAlignment: WrapCrossAlignment.center, children: [ UserFullNameWidget( - context, - widget.privateMessages[index].creator?.name, - widget.privateMessages[index].creator?.displayName, - fetchInstanceNameFromUrl(widget.privateMessages[index].creator?.actorId), - includeInstance: true, - ), + name: widget.privateMessages[index].creator?.name, + displayName: widget.privateMessages[index].creator?.displayName, + instance: fetchInstanceNameFromUrl(widget.privateMessages[index].creator?.actorId), + includeInstance: true), const Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Icon(Icons.arrow_forward_rounded, size: 14), ), UserFullNameWidget( - context, - widget.privateMessages[index].recipient?.name, - widget.privateMessages[index].recipient?.displayName, - fetchInstanceNameFromUrl(widget.privateMessages[index].recipient?.actorId), - includeInstance: true, - ), + name: widget.privateMessages[index].recipient?.name, + displayName: widget.privateMessages[index].recipient?.displayName, + instance: fetchInstanceNameFromUrl(widget.privateMessages[index].recipient?.actorId), + includeInstance: true), ], ), ), diff --git a/lib/src/features/instance/presentation/widgets/instance_information.dart b/lib/src/features/instance/presentation/widgets/instance_information.dart index aa677bc83..0b0944f3f 100644 --- a/lib/src/features/instance/presentation/widgets/instance_information.dart +++ b/lib/src/features/instance/presentation/widgets/instance_information.dart @@ -4,8 +4,8 @@ import 'package:intl/intl.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/instance_avatar.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/instance_avatar.dart'; import 'package:thunder/packages/ui/ui.dart' show ThunderDivider; /// A widget that displays information about a given instance. diff --git a/lib/src/features/instance/presentation/widgets/instance_list_entry.dart b/lib/src/features/instance/presentation/widgets/instance_list_entry.dart index 4fd8a24ce..a3e3544ed 100644 --- a/lib/src/features/instance/presentation/widgets/instance_list_entry.dart +++ b/lib/src/features/instance/presentation/widgets/instance_list_entry.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/instance_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/instance_avatar.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; /// Creates a widget which can display a summary of an instance for a list. diff --git a/lib/src/features/moderator/presentation/pages/report_page.dart b/lib/src/features/moderator/presentation/pages/report_page.dart index b9e5865d0..606ee46c0 100644 --- a/lib/src/features/moderator/presentation/pages/report_page.dart +++ b/lib/src/features/moderator/presentation/pages/report_page.dart @@ -13,9 +13,9 @@ import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/moderator/moderator.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:thunder/src/features/comment/presentation/widgets/comment_reference.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -236,11 +236,9 @@ class _ReportFeedViewState extends State { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: UserFullNameWidget( - context, - state.postReports[index].creator?.name ?? '', - state.postReports[index].creator?.displayName ?? '', - fetchInstanceNameFromUrl(state.postReports[index].creator?.actorId ?? ''), - ), + name: state.postReports[index].creator?.name ?? '', + displayName: state.postReports[index].creator?.displayName ?? '', + instance: fetchInstanceNameFromUrl(state.postReports[index].creator?.actorId ?? '')), ), ), ], @@ -254,7 +252,7 @@ class _ReportFeedViewState extends State { l10n.detailedReason(state.postReports[index].reason), maxLines: 4, overflow: TextOverflow.ellipsis, - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, fontWeight: FontWeight.w600, @@ -339,11 +337,9 @@ class _ReportFeedViewState extends State { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: UserFullNameWidget( - context, - state.commentReports[index].creator?.name, - state.commentReports[index].creator?.displayName, - fetchInstanceNameFromUrl(state.commentReports[index].creator?.actorId), - ), + name: state.commentReports[index].creator?.name, + displayName: state.commentReports[index].creator?.displayName, + instance: fetchInstanceNameFromUrl(state.commentReports[index].creator?.actorId)), ), ), ], @@ -357,7 +353,7 @@ class _ReportFeedViewState extends State { l10n.detailedReason(state.commentReports[index].reason), maxLines: 4, overflow: TextOverflow.ellipsis, - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.error, fontWeight: FontWeight.w600, diff --git a/lib/src/features/modlog/presentation/widgets/modlog_filter_picker.dart b/lib/src/features/modlog/presentation/widgets/modlog_filter_picker.dart index 12e82ff9c..60fc66a24 100644 --- a/lib/src/features/modlog/presentation/widgets/modlog_filter_picker.dart +++ b/lib/src/features/modlog/presentation/widgets/modlog_filter_picker.dart @@ -157,7 +157,7 @@ class _ModlogActionTypePickerState extends State { physics: const NeverScrollableScrollPhysics(), children: [ ...defaultModlogActionTypeItems.map( - (item) => PickerItem( + (item) => PickerItem( label: item.label, icon: item.icon, onSelected: () { @@ -291,7 +291,7 @@ class ModlogSubFilterPicker extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), children: items .map( - (item) => PickerItem( + (item) => PickerItem( label: item.label, icon: item.icon, onSelected: () { diff --git a/lib/src/features/modlog/presentation/widgets/modlog_item_card.dart b/lib/src/features/modlog/presentation/widgets/modlog_item_card.dart index fca783feb..69e9f5e61 100644 --- a/lib/src/features/modlog/presentation/widgets/modlog_item_card.dart +++ b/lib/src/features/modlog/presentation/widgets/modlog_item_card.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/modlog/modlog.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/packages/ui/ui.dart' show ThunderDivider; @@ -80,7 +80,7 @@ class ModlogItemCard extends StatelessWidget { ScalableText( event.getModlogEventTypeName(), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - fontScale: titleFontSizeScale, + textScaleFactor: titleFontSizeScale.textScaleFactor, ), ], ), @@ -105,7 +105,7 @@ class ModlogItemCard extends StatelessWidget { l10n.detailedReason('${event.reason}'), maxLines: 4, overflow: TextOverflow.ellipsis, - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.90), ), diff --git a/lib/src/features/modlog/presentation/widgets/modlog_item_context_card.dart b/lib/src/features/modlog/presentation/widgets/modlog_item_context_card.dart index 62526136e..338483a2e 100644 --- a/lib/src/features/modlog/presentation/widgets/modlog_item_context_card.dart +++ b/lib/src/features/modlog/presentation/widgets/modlog_item_context_card.dart @@ -8,12 +8,12 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/modlog/modlog.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar; @@ -115,7 +115,7 @@ class ModlogPostItemContextCard extends StatelessWidget { ScalableText( HtmlUnescape().convert(post.name), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - fontScale: titleFontSizeScale, + textScaleFactor: titleFontSizeScale.textScaleFactor, ), Padding( padding: const EdgeInsets.only(right: 6.0, top: 6.0), @@ -123,13 +123,11 @@ class ModlogPostItemContextCard extends StatelessWidget { borderRadius: BorderRadius.circular(6), onTap: () => navigateToFeedPage(context, feedType: FeedType.community, communityId: community?.id), child: CommunityFullNameWidget( - context, - community?.name, - community?.title, - fetchInstanceNameFromUrl(community?.actorId), - fontScale: metadataFontSizeScale, - transformColor: (color) => color?.withValues(alpha: 0.75), - ), + name: community?.name, + displayName: community?.title, + instance: fetchInstanceNameFromUrl(community?.actorId), + fontScale: metadataFontSizeScale, + transformColor: (color) => color?.withValues(alpha: 0.75)), ), ), ], @@ -242,7 +240,7 @@ class _ModlogCommentItemContextCardState extends State navigateToFeedPage(context, feedType: FeedType.user, userId: widget.user?.id), child: ScalableText( '${widget.user?.displayName ?? widget.user?.displayNameOrName}', - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith(color: textStyleCommunityAndAuthor(theme.textTheme.bodyMedium?.color)), ), ), ScalableText( ' in ', - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.4), @@ -280,13 +278,11 @@ class _ModlogCommentItemContextCardState extends State navigateToFeedPage(context, feedType: FeedType.community, communityId: widget.community?.id), child: CommunityFullNameWidget( - context, - widget.community?.name, - widget.community?.title, - fetchInstanceNameFromUrl(widget.community?.actorId), - fontScale: metadataFontSizeScale, - transformColor: textStyleCommunityAndAuthor, - ), + name: widget.community?.name, + displayName: widget.community?.title, + instance: fetchInstanceNameFromUrl(widget.community?.actorId), + fontScale: metadataFontSizeScale, + transformColor: textStyleCommunityAndAuthor), ), ], ), @@ -338,15 +334,10 @@ class ModlogUserItemContextCard extends StatelessWidget { ScalableText( HtmlUnescape().convert(user?.displayName ?? user?.displayNameOrName ?? l10n.user), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - fontScale: titleFontSizeScale, + textScaleFactor: titleFontSizeScale.textScaleFactor, ), UserFullNameWidget( - context, - user?.displayNameOrName, - user?.displayName, - fetchInstanceNameFromUrl(user?.actorId), - transformColor: (color) => color?.withValues(alpha: 0.75), - ), + name: user?.displayNameOrName, displayName: user?.displayName, instance: fetchInstanceNameFromUrl(user?.actorId), transformColor: (color) => color?.withValues(alpha: 0.75)), ], ), ], @@ -396,16 +387,14 @@ class ModlogCommunityItemContextCard extends StatelessWidget { ScalableText( HtmlUnescape().convert(community?.title ?? community?.name ?? l10n.community), style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), - fontScale: titleFontSizeScale, + textScaleFactor: titleFontSizeScale.textScaleFactor, ), CommunityFullNameWidget( - context, - community?.name, - community?.title, - fetchInstanceNameFromUrl(community?.actorId), - fontScale: metadataFontSizeScale, - transformColor: (color) => color?.withValues(alpha: 0.75), - ), + name: community?.name, + displayName: community?.title, + instance: fetchInstanceNameFromUrl(community?.actorId), + fontScale: metadataFontSizeScale, + transformColor: (color) => color?.withValues(alpha: 0.75)), ], ), ], diff --git a/lib/src/features/notification/presentation/utils/notification_settings_utils.dart b/lib/src/features/notification/presentation/utils/notification_settings_utils.dart index 83d9eee92..7742dd17f 100644 --- a/lib/src/features/notification/presentation/utils/notification_settings_utils.dart +++ b/lib/src/features/notification/presentation/utils/notification_settings_utils.dart @@ -12,7 +12,7 @@ import 'package:unifiedpush/unifiedpush.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/notification/notification.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar, showThunderDialog; /// This function is used to update the notification settings. It is called when the user changes the notification settings. diff --git a/lib/src/features/post/presentation/pages/create_post_page.dart b/lib/src/features/post/presentation/pages/create_post_page.dart index 47388bb23..5b381903d 100644 --- a/lib/src/features/post/presentation/pages/create_post_page.dart +++ b/lib/src/features/post/presentation/pages/create_post_page.dart @@ -22,19 +22,20 @@ import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/drafts/drafts.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/search/search.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/features/post/presentation/widgets/cross_posts.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/shared/language_selector.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_view.dart'; +import 'package:thunder/src/shared/content/widgets/media/media_view.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/shared/theme/color_utils.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show isImageUrl, selectImagesToUpload, showSnackbar; +import 'package:thunder/packages/ui/ui.dart' show showSnackbar; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart' show isImageUrl, selectImagesToUpload; class CreatePostPage extends StatefulWidget { /// The account to use for composing this post. @@ -944,13 +945,12 @@ class _CommunitySelectorState extends State { children: [ Text('${widget.community!.title} '), CommunityFullNameWidget( - context, - widget.community!.name, - widget.community!.title, - fetchInstanceNameFromUrl(widget.community!.actorId), - // Override, because we have the display name right above + name: widget.community!.name, + displayName: widget.community!.title, + instance: fetchInstanceNameFromUrl(widget.community!.actorId), + // Override because we have the display name right above. useDisplayName: false, - ) + ), ], ) : SizedBox( diff --git a/lib/src/features/post/presentation/pages/post_page.dart b/lib/src/features/post/presentation/pages/post_page.dart index 211e903f8..afa47dbbc 100644 --- a/lib/src/features/post/presentation/pages/post_page.dart +++ b/lib/src/features/post/presentation/pages/post_page.dart @@ -17,7 +17,7 @@ import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/post/presentation/widgets/cross_posts.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/shared/widgets/text/selectable_text_modal.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; @@ -473,7 +473,7 @@ class _PostPageFeedEndState extends State<_PostPageFeedEnd> { padding: const EdgeInsets.symmetric(vertical: 32.0), child: ScalableText( comments.isEmpty ? l10n.noCommentsFound : l10n.endOfComments, - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, textAlign: TextAlign.center, style: theme.textTheme.titleSmall, ), diff --git a/lib/src/features/post/presentation/utils/post_media_utils.dart b/lib/src/features/post/presentation/utils/post_media_utils.dart index f1bbb5adf..605f5cb5f 100644 --- a/lib/src/features/post/presentation/utils/post_media_utils.dart +++ b/lib/src/features/post/presentation/utils/post_media_utils.dart @@ -8,8 +8,8 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/search/search.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show getScaledMediaSize, isImageUrl, isVideoUrl, retrieveImageDimensions; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart' show getScaledMediaSize, isImageUrl, isVideoUrl, retrieveImageDimensions; final _htmlUnescape = HtmlUnescape(); diff --git a/lib/src/features/post/presentation/widgets/cross_posts.dart b/lib/src/features/post/presentation/widgets/cross_posts.dart index 997a21df8..c1e84e432 100644 --- a/lib/src/features/post/presentation/widgets/cross_posts.dart +++ b/lib/src/features/post/presentation/widgets/cross_posts.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/post/api.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; @@ -72,12 +72,10 @@ class _CrossPostsState extends State { SizedBox(width: 4.0), Flexible( child: CommunityFullNameWidget( - context, - widget.crossPosts[index].community?.name, - widget.crossPosts[index].community?.title, - fetchInstanceNameFromUrl(widget.crossPosts[index].community?.actorId), - textStyle: crossPostLinkTextStyle, - ), + name: widget.crossPosts[index].community?.name, + displayName: widget.crossPosts[index].community?.title, + instance: fetchInstanceNameFromUrl(widget.crossPosts[index].community?.actorId), + textStyle: crossPostLinkTextStyle), ), ], ), @@ -118,12 +116,10 @@ class _CrossPostsState extends State { if (!_areCrossPostsExpanded) WidgetSpan( child: CommunityFullNameWidget( - context, - widget.crossPosts[0].community?.name, - widget.crossPosts[0].community?.title, - fetchInstanceNameFromUrl(widget.crossPosts[0].community?.actorId), - textStyle: theme.textTheme.bodySmall?.copyWith(color: crossPostLinkTextStyle?.color), - ), + name: widget.crossPosts[0].community?.name, + displayName: widget.crossPosts[0].community?.title, + instance: fetchInstanceNameFromUrl(widget.crossPosts[0].community?.actorId), + textStyle: theme.textTheme.bodySmall?.copyWith(color: crossPostLinkTextStyle?.color)), ), TextSpan( text: _areCrossPostsExpanded || widget.crossPosts.length == 1 ? '' : ' ${l10n.andXMore(widget.crossPosts.length - 1)}', diff --git a/lib/src/features/post/presentation/widgets/post_body/post_body.dart b/lib/src/features/post/presentation/widgets/post_body/post_body.dart index 2f1a06eb9..89ca6f623 100644 --- a/lib/src/features/post/presentation/widgets/post_body/post_body.dart +++ b/lib/src/features/post/presentation/widgets/post_body/post_body.dart @@ -18,11 +18,11 @@ import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/features/post/presentation/widgets/cross_posts.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_view.dart'; +import 'package:thunder/src/shared/content/widgets/media/media_view.dart'; import 'package:thunder/src/shared/reply_to_preview_actions.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/packages/ui/ui.dart' show ConditionalParentWidget; @@ -198,7 +198,7 @@ class _PostBodyState extends State with SingleTickerProviderStateMixin ? ScalableText( post.body ?? '', style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, ) : CommonMarkdownBody(body: post.body ?? '', nsfw: post.nsfw && hideNsfwPreviews), ), diff --git a/lib/src/features/post/presentation/widgets/post_body/post_body_preview.dart b/lib/src/features/post/presentation/widgets/post_body/post_body_preview.dart index 5610d4119..920ae2b8d 100644 --- a/lib/src/features/post/presentation/widgets/post_body/post_body_preview.dart +++ b/lib/src/features/post/presentation/widgets/post_body/post_body_preview.dart @@ -5,8 +5,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; /// Provides a preview of the post body when the post is collapsed. /// @@ -57,7 +57,7 @@ class PostBodyPreview extends StatelessWidget { ? ScalableText( post.body ?? '', style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, ) : CommonMarkdownBody(body: post.body ?? '', nsfw: post.nsfw && hideNsfwPreviews); diff --git a/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart b/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart index 85c43459a..fcb3aa35d 100644 --- a/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart +++ b/lib/src/features/post/presentation/widgets/post_body/post_body_title.dart @@ -4,12 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/widgets/chips/community_chip.dart'; import 'package:thunder/src/shared/widgets/chips/user_chip.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/compact_thumbnail_preview.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/src/shared/content/widgets/media/compact_thumbnail_preview.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -111,7 +111,7 @@ class PostBodyTitle extends StatelessWidget { return ScalableText( post.name, - fontScale: titleFontSizeScale, + textScaleFactor: titleFontSizeScale.textScaleFactor, style: theme.textTheme.titleMedium, ); } diff --git a/lib/src/features/post/presentation/widgets/post_page_fab.dart b/lib/src/features/post/presentation/widgets/post_page_fab.dart index 0a53fbb4b..5c81926bf 100644 --- a/lib/src/features/post/presentation/widgets/post_page_fab.dart +++ b/lib/src/features/post/presentation/widgets/post_page_fab.dart @@ -14,10 +14,9 @@ import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/shared/sort_picker.dart'; import 'package:thunder/src/shared/gesture_fab.dart'; -import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/shared/widgets/comment_navigator_fab.dart'; -import 'package:thunder/packages/ui/ui.dart' show showSnackbar; +import 'package:thunder/packages/ui/ui.dart' show showSnackbar, showThunderTypeaheadDialog; /// The FAB for the post page. class PostPageFAB extends StatefulWidget { @@ -100,10 +99,12 @@ class _PostPageFABState extends State { return; } - showInputDialog( + showThunderTypeaheadDialog( context: context, title: l10n.searchComments, inputLabel: l10n.searchTerm, + primaryButtonText: l10n.ok, + secondaryButtonText: l10n.cancel, onSubmitted: ({payload, value}) { Navigator.of(context).pop(); Map commentSearchResults = {}; diff --git a/lib/src/features/settings/application/state/theme_preferences_cubit/theme_preferences_cubit.dart b/lib/src/features/settings/application/state/theme_preferences_cubit/theme_preferences_cubit.dart index c7a1c1798..e9a4e0aec 100644 --- a/lib/src/features/settings/application/state/theme_preferences_cubit/theme_preferences_cubit.dart +++ b/lib/src/features/settings/application/state/theme_preferences_cubit/theme_preferences_cubit.dart @@ -7,7 +7,7 @@ import 'package:equatable/equatable.dart'; import 'package:thunder/src/foundation/contracts/contracts.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/packages/ui/ui.dart' show FullNameSeparator, NameColor, NameThickness; +import 'package:thunder/src/shared/identity/models/name_style.dart' show FullNameSeparator, NameColor, NameThickness; part 'theme_preferences_state.dart'; diff --git a/lib/src/features/settings/domain/full_name.dart b/lib/src/features/settings/domain/full_name.dart index d1cab8e57..6aac63fa0 100644 --- a/lib/src/features/settings/domain/full_name.dart +++ b/lib/src/features/settings/domain/full_name.dart @@ -1,145 +1,39 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/packages/ui/ui.dart' - show - CommunityFullNameWidget, - FullNameSeparator, - NameColor, - NameThickness, - UserFullNameWidget, - formatCommunityFullNamePrefix, - formatCommunityFullNameSuffix, - formatUserFullNamePrefix, - formatUserFullNameSuffix; +import 'package:thunder/src/shared/identity/models/name_style.dart' show FullNameSeparator; +import 'package:thunder/src/shared/identity/utils/name_formatting.dart' show formatCommunityFullNamePrefix, formatCommunityFullNameSuffix, formatUserFullNamePrefix, formatUserFullNameSuffix; -export 'package:thunder/packages/ui/ui.dart' show FullNameSeparator, NameColor, NameThickness; - -/// --- SAMPLES --- - -String generateSampleUserFullName(FullNameSeparator separator, bool useDisplayName) => generateUserFullName( - null, - 'name', - 'name', - 'instance.tld', - userSeparator: separator, - useDisplayName: useDisplayName, - ); - -Widget generateSampleUserFullNameWidget( - FullNameSeparator separator, { - NameThickness? userNameThickness, - NameColor? userNameColor, - NameThickness? instanceNameThickness, - NameColor? instanceNameColor, - TextStyle? textStyle, - bool? useDisplayName, -}) => - UserFullNameWidget( - name: 'name', - displayName: 'name', - instance: 'instance.tld', - separator: separator, - useDisplayName: useDisplayName ?? false, - userNameThickness: userNameThickness ?? NameThickness.normal, - userNameColor: userNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), - instanceNameThickness: instanceNameThickness ?? NameThickness.light, - instanceNameColor: instanceNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), - textStyle: textStyle, - textScaleFactor: FontScale.base.textScaleFactor, - ); - -String generateSampleCommunityFullName(FullNameSeparator separator, bool useDisplayName) => generateCommunityFullName( - null, - 'name', - 'name', - 'instance.tld', - communitySeparator: separator, - useDisplayName: useDisplayName, - ); - -Widget generateSampleCommunityFullNameWidget( - FullNameSeparator separator, { - NameThickness? communityNameThickness, - NameColor? communityNameColor, - NameThickness? instanceNameThickness, - NameColor? instanceNameColor, - TextStyle? textStyle, - bool? useDisplayName, -}) => - CommunityFullNameWidget( - name: 'name', - displayName: 'name', - instance: 'instance.tld', - separator: separator, - useDisplayName: useDisplayName ?? false, - communityNameThickness: communityNameThickness ?? NameThickness.normal, - communityNameColor: communityNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), - instanceNameThickness: instanceNameThickness ?? NameThickness.light, - instanceNameColor: instanceNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), - textStyle: textStyle, - textScaleFactor: FontScale.base.textScaleFactor, - ); +export 'package:thunder/src/shared/identity/models/name_style.dart' show FullNameSeparator, NameColor, NameThickness; /// --- USERS --- -String generateUserFullNamePrefix(BuildContext? context, String? name, String? displayName, {FullNameSeparator? userSeparator, bool? useDisplayName}) { +String generateUserFullName(BuildContext? context, String? name, String? displayName, String? instance, {FullNameSeparator? userSeparator, bool? useDisplayName}) { assert(context != null || (userSeparator != null && useDisplayName != null)); - final resolvedSeparator = userSeparator ?? context!.read().state.userSeparator; - final resolvedUseDisplayName = useDisplayName ?? context!.read().state.useDisplayNamesForUsers; - - return formatUserFullNamePrefix( - name, - displayName, - separator: resolvedSeparator, - useDisplayName: resolvedUseDisplayName, - ); -} + final preferences = context?.read().state; + final resolvedSeparator = userSeparator ?? preferences!.userSeparator; + final resolvedUseDisplayName = useDisplayName ?? preferences!.useDisplayNamesForUsers; -String generateUserFullNameSuffix(BuildContext? context, String? instance, {FullNameSeparator? userSeparator}) { - assert(context != null || userSeparator != null); + final prefix = formatUserFullNamePrefix(name, displayName, separator: resolvedSeparator, useDisplayName: resolvedUseDisplayName); + final suffix = formatUserFullNameSuffix(instance, separator: resolvedSeparator); - final resolvedSeparator = userSeparator ?? context!.read().state.userSeparator; - - return formatUserFullNameSuffix(instance, separator: resolvedSeparator); -} - -String generateUserFullName(BuildContext? context, String? name, String? displayName, String? instance, {FullNameSeparator? userSeparator, bool? useDisplayName}) { - final prefix = generateUserFullNamePrefix(context, name, displayName, userSeparator: userSeparator, useDisplayName: useDisplayName); - final suffix = generateUserFullNameSuffix(context, instance, userSeparator: userSeparator); return '$prefix$suffix'; } /// --- COMMUNITIES --- -String generateCommunityFullNamePrefix(BuildContext? context, String? name, String? displayName, {FullNameSeparator? communitySeparator, bool? useDisplayName}) { +String generateCommunityFullName(BuildContext? context, String? name, String? displayName, String? instance, {FullNameSeparator? communitySeparator, bool? useDisplayName}) { assert(context != null || (communitySeparator != null && useDisplayName != null)); - final resolvedSeparator = communitySeparator ?? context!.read().state.communitySeparator; - final resolvedUseDisplayName = useDisplayName ?? context!.read().state.useDisplayNamesForCommunities; - - return formatCommunityFullNamePrefix( - name, - displayName, - separator: resolvedSeparator, - useDisplayName: resolvedUseDisplayName, - ); -} + final preferences = context?.read().state; + final resolvedSeparator = communitySeparator ?? preferences!.communitySeparator; + final resolvedUseDisplayName = useDisplayName ?? preferences!.useDisplayNamesForCommunities; -String generateCommunityFullNameSuffix(BuildContext? context, String? instance, {FullNameSeparator? communitySeparator}) { - assert(context != null || communitySeparator != null); + final prefix = formatCommunityFullNamePrefix(name, displayName, separator: resolvedSeparator, useDisplayName: resolvedUseDisplayName); + final suffix = formatCommunityFullNameSuffix(instance, separator: resolvedSeparator); - final resolvedSeparator = communitySeparator ?? context!.read().state.communitySeparator; - - return formatCommunityFullNameSuffix(instance, separator: resolvedSeparator); -} - -String generateCommunityFullName(BuildContext? context, String? name, String? displayName, String? instance, {FullNameSeparator? communitySeparator, bool? useDisplayName}) { - final prefix = generateCommunityFullNamePrefix(context, name, displayName, communitySeparator: communitySeparator, useDisplayName: useDisplayName); - final suffix = generateCommunityFullNameSuffix(context, instance, communitySeparator: communitySeparator); return '$prefix$suffix'; } diff --git a/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart b/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart index 7b11b9baa..36c309d5a 100644 --- a/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/accessibility_settings_page.dart @@ -7,10 +7,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; +import 'package:thunder/packages/ui/ui.dart'; class AccessibilitySettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -107,17 +107,16 @@ class _AccessibilitySettingsPageState extends State w style: theme.textTheme.titleLarge, ), ), - ToggleOption( - description: l10n.reduceAnimations, - subtitle: l10n.reducesAnimations, - value: reduceAnimations, - iconEnabled: Icons.animation, - iconDisabled: Icons.animation, - onToggle: (bool value) => setPreferences(LocalSettings.reduceAnimations, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.reduceAnimations, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.reduceAnimations, + subtitle: l10n.reducesAnimations, + value: reduceAnimations, + iconEnabled: Icons.animation, + iconDisabled: Icons.animation, + onChanged: (bool value) => setPreferences(LocalSettings.reduceAnimations, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.reduceAnimations), + highlighted: settingToHighlight == LocalSettings.reduceAnimations), ], ), ), diff --git a/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart b/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart index c9394c753..ab36513cd 100644 --- a/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/appearance/comment_appearance_settings_page.dart @@ -9,14 +9,14 @@ import 'package:thunder/src/app/wiring/state_factories.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show ListPickerItem, showThunderDialog; +import 'package:thunder/packages/ui/ui.dart'; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; class CommentAppearanceSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -327,88 +327,80 @@ class _CommentAppearanceSettingsPageState extends State setPreferences(LocalSettings.showCommentActionButtons, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showCommentActionButtons, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.combineCommentScoresLabel, - value: combineCommentScores, - iconEnabled: Icons.onetwothree_rounded, - iconDisabled: Icons.onetwothree_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.combineCommentScores, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.combineCommentScores, - highlightedSetting: settingToHighlight, - ), - - ToggleOption( - description: l10n.commentShowUserInstance, - value: commentShowUserInstance, - iconEnabled: Icons.dns_sharp, - iconDisabled: Icons.dns_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.commentShowUserInstance, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.commentShowUserInstance, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.commentShowUserAvatar, - value: commentShowUserAvatar, - iconEnabled: Icons.account_circle, - iconDisabled: Icons.account_circle_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.commentShowUserAvatar, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.commentShowUserAvatar, - highlightedSetting: settingToHighlight, - ), - - ListOption( - description: l10n.nestedCommentIndicatorStyle, - value: ListPickerItem(label: nestedIndicatorStyle.value, icon: Icons.local_fire_department_rounded, payload: nestedIndicatorStyle), - options: [ - ListPickerItem(icon: Icons.view_list_rounded, label: NestedCommentIndicatorStyle.thick.value, payload: NestedCommentIndicatorStyle.thick), - ListPickerItem(icon: Icons.format_list_bulleted_rounded, label: NestedCommentIndicatorStyle.thin.value, payload: NestedCommentIndicatorStyle.thin), - ], - icon: Icons.format_list_bulleted_rounded, - onChanged: (value) async => setPreferences(LocalSettings.nestedCommentIndicatorStyle, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.nestedCommentIndicatorStyle, - highlightedSetting: settingToHighlight, - ), - - ListOption( - description: l10n.nestedCommentIndicatorColor, - value: ListPickerItem(label: nestedIndicatorColor.value, icon: Icons.local_fire_department_rounded, payload: nestedIndicatorColor), - options: [ - ListPickerItem(icon: Icons.invert_colors_on_rounded, label: NestedCommentIndicatorColor.colorful.value, payload: NestedCommentIndicatorColor.colorful), - ListPickerItem(icon: Icons.invert_colors_off_rounded, label: NestedCommentIndicatorColor.monochrome.value, payload: NestedCommentIndicatorColor.monochrome), - ], - icon: Icons.color_lens_outlined, - onChanged: (value) async => setPreferences(LocalSettings.nestedCommentIndicatorColor, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.nestedCommentIndicatorColor, - highlightedSetting: settingToHighlight, - ), - - SettingsListTile( - icon: Icons.alternate_email_rounded, - description: l10n.usernameFormattingRedirect, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAppearanceTheming, settingToHighlight: LocalSettings.userStyle), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.showCommentActionButtons, + value: showCommentButtonActions, + iconEnabled: Icons.mode_comment_rounded, + iconDisabled: Icons.mode_comment_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.showCommentActionButtons, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showCommentActionButtons), + highlighted: settingToHighlight == LocalSettings.showCommentActionButtons), + ThunderToggleOption( + title: l10n.combineCommentScoresLabel, + value: combineCommentScores, + iconEnabled: Icons.onetwothree_rounded, + iconDisabled: Icons.onetwothree_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.combineCommentScores, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.combineCommentScores), + highlighted: settingToHighlight == LocalSettings.combineCommentScores), + + ThunderToggleOption( + title: l10n.commentShowUserInstance, + value: commentShowUserInstance, + iconEnabled: Icons.dns_sharp, + iconDisabled: Icons.dns_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.commentShowUserInstance, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.commentShowUserInstance), + highlighted: settingToHighlight == LocalSettings.commentShowUserInstance), + ThunderToggleOption( + title: l10n.commentShowUserAvatar, + value: commentShowUserAvatar, + iconEnabled: Icons.account_circle, + iconDisabled: Icons.account_circle_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.commentShowUserAvatar, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.commentShowUserAvatar), + highlighted: settingToHighlight == LocalSettings.commentShowUserAvatar), + + ThunderListOption( + title: l10n.nestedCommentIndicatorStyle, + value: ListPickerItem(label: nestedIndicatorStyle.value, icon: Icons.local_fire_department_rounded, payload: nestedIndicatorStyle), + options: [ + ListPickerItem(icon: Icons.view_list_rounded, label: NestedCommentIndicatorStyle.thick.value, payload: NestedCommentIndicatorStyle.thick), + ListPickerItem(icon: Icons.format_list_bulleted_rounded, label: NestedCommentIndicatorStyle.thin.value, payload: NestedCommentIndicatorStyle.thin), + ], + leading: Icon(Icons.format_list_bulleted_rounded), + onChanged: (value) async => setPreferences(LocalSettings.nestedCommentIndicatorStyle, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.nestedCommentIndicatorStyle), + highlighted: settingToHighlight == LocalSettings.nestedCommentIndicatorStyle), + + ThunderListOption( + title: l10n.nestedCommentIndicatorColor, + value: ListPickerItem(label: nestedIndicatorColor.value, icon: Icons.local_fire_department_rounded, payload: nestedIndicatorColor), + options: [ + ListPickerItem(icon: Icons.invert_colors_on_rounded, label: NestedCommentIndicatorColor.colorful.value, payload: NestedCommentIndicatorColor.colorful), + ListPickerItem(icon: Icons.invert_colors_off_rounded, label: NestedCommentIndicatorColor.monochrome.value, payload: NestedCommentIndicatorColor.monochrome), + ], + leading: Icon(Icons.color_lens_outlined), + onChanged: (value) async => setPreferences(LocalSettings.nestedCommentIndicatorColor, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.nestedCommentIndicatorColor), + highlighted: settingToHighlight == LocalSettings.nestedCommentIndicatorColor), + + ThunderSettingsTile( + leading: Icon(Icons.alternate_email_rounded), + title: l10n.usernameFormattingRedirect, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAppearanceTheming, settingToHighlight: LocalSettings.userStyle), + highlightKey: settingToHighlightKey, + highlighted: false), SizedBox(height: 128.0), ], diff --git a/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart b/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart index aaad846cc..0b95ef10e 100644 --- a/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/appearance/post_appearance_settings_page.dart @@ -15,13 +15,12 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/post/post.dart'; -import 'package:thunder/src/features/settings/settings.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; -import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, ListPickerItem, showThunderDialog; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; class PostAppearanceSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -603,214 +602,204 @@ class _PostAppearanceSettingsPageState extends State padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.feedSettings, style: theme.textTheme.titleMedium), ), - ListOption( - description: l10n.postViewType, - value: ListPickerItem(label: useCompactView ? l10n.compactView : l10n.cardView, icon: Icons.crop_16_9_rounded, payload: useCompactView), - options: [ - ListPickerItem(icon: Icons.crop_16_9_rounded, label: l10n.compactView, payload: true), - ListPickerItem(icon: Icons.crop_din_rounded, label: l10n.cardView, payload: false), - ], - icon: Icons.view_list_rounded, - onChanged: (value) async => setPreferences(LocalSettings.useCompactView, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.useCompactView, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.hideNsfwPreviews, - value: hideNsfwPreviews, - iconEnabled: Icons.no_adult_content, - iconDisabled: Icons.no_adult_content, - onToggle: (bool value) => setPreferences(LocalSettings.hideNsfwPreviews, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.hideNsfwPreviews, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.hideThumbnails, - value: hideThumbnails, - iconEnabled: Icons.hide_image_outlined, - iconDisabled: Icons.image_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.hideThumbnails, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.hideThumbnails, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostCommunityFirst, - value: showCommunityFirst, - iconEnabled: Icons.vertical_align_top_rounded, - iconDisabled: Icons.vertical_align_bottom_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.showPostCommunityFirst, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostCommunityFirst, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostCommunityIcons, - value: showCommunityIcons, - iconEnabled: Icons.groups, - iconDisabled: Icons.groups, - onToggle: (bool value) => setPreferences(LocalSettings.showPostCommunityIcons, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostCommunityIcons, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostAuthor, - subtitle: l10n.showPostAuthorSubtitle, - value: showPostAuthor, - iconEnabled: Icons.person_rounded, - iconDisabled: Icons.person_off_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.showPostAuthor, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostAuthor, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showUserInstance, - value: postShowUserInstance, - iconEnabled: Icons.dns_sharp, - iconDisabled: Icons.dns_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.postShowUserInstance, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postShowUserInstance, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.dimReadPosts, - subtitle: l10n.dimReadPosts, - value: dimReadPosts, - iconEnabled: Icons.chrome_reader_mode, - iconDisabled: Icons.chrome_reader_mode_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.dimReadPosts, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.dimReadPosts, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showFullDate, - subtitle: l10n.showFullDateDescription, - value: showFullPostDate, - iconEnabled: Icons.date_range_rounded, - iconDisabled: Icons.date_range_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.showFullPostDate, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showFullPostDate, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.dateFormat, - disabled: !showFullPostDate, - value: ListPickerItem( - label: (selectedDateFormat == null || selectedDateFormat!.pattern == dateFormats.first.pattern) ? l10n.system : selectedDateFormat!.pattern!, - icon: Icons.access_time_filled_rounded, - payload: selectedDateFormat, - capitalizeLabel: false, - ), - options: dateFormats - .map( - (DateFormat dateFormat) => ListPickerItem( - icon: Icons.access_time_filled_rounded, - label: dateFormat.format(DateTime.now()), - payload: dateFormat, - subtitle: dateFormat.pattern == dateFormats.first.pattern ? l10n.system : dateFormat.pattern, - ), - ) - .toList(), - icon: Icons.access_time_filled_rounded, - onChanged: (value) async => setPreferences(LocalSettings.dateFormat, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.dateFormat, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.dividerAppearance, - value: const ListPickerItem(payload: -1), - icon: Icons.splitscreen_rounded, - highlightKey: settingToHighlightKey, - setting: LocalSettings.dividerAppearance, - highlightedSetting: settingToHighlight, - customListPicker: StatefulBuilder( - builder: (context, setState) { - return BottomSheetListPicker( - title: l10n.dividerAppearance, - heading: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(l10n.preview, style: theme.textTheme.titleMedium), - const SizedBox(height: 20.0), - const FeedCardDivider(), - const SizedBox(height: 16.0), - ], - ), - items: [ - ListPickerItem( - customWidget: ListTile( - title: Text(l10n.thickness), - contentPadding: const EdgeInsets.only(left: 24.0, right: 20.0), - trailing: DropdownButton( - value: feedCardDividerThickness, - underline: const SizedBox(), - items: FeedCardDividerThickness.values.map((e) => DropdownMenuItem(value: e, child: Text(e.label))).toList(), - onChanged: (FeedCardDividerThickness? value) { - setPreferences(LocalSettings.feedCardDividerThickness, value); - setState(() {}); // Trigger rebuild - }, + ThunderListOption( + title: l10n.postViewType, + value: ListPickerItem(label: useCompactView ? l10n.compactView : l10n.cardView, icon: Icons.crop_16_9_rounded, payload: useCompactView), + options: [ + ListPickerItem(icon: Icons.crop_16_9_rounded, label: l10n.compactView, payload: true), + ListPickerItem(icon: Icons.crop_din_rounded, label: l10n.cardView, payload: false), + ], + leading: Icon(Icons.view_list_rounded), + onChanged: (value) async => setPreferences(LocalSettings.useCompactView, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.useCompactView), + highlighted: settingToHighlight == LocalSettings.useCompactView), + ThunderToggleOption( + title: l10n.hideNsfwPreviews, + value: hideNsfwPreviews, + iconEnabled: Icons.no_adult_content, + iconDisabled: Icons.no_adult_content, + onChanged: (bool value) => setPreferences(LocalSettings.hideNsfwPreviews, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.hideNsfwPreviews), + highlighted: settingToHighlight == LocalSettings.hideNsfwPreviews), + ThunderToggleOption( + title: l10n.hideThumbnails, + value: hideThumbnails, + iconEnabled: Icons.hide_image_outlined, + iconDisabled: Icons.image_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.hideThumbnails, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.hideThumbnails), + highlighted: settingToHighlight == LocalSettings.hideThumbnails), + ThunderToggleOption( + title: l10n.showPostCommunityFirst, + value: showCommunityFirst, + iconEnabled: Icons.vertical_align_top_rounded, + iconDisabled: Icons.vertical_align_bottom_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.showPostCommunityFirst, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostCommunityFirst), + highlighted: settingToHighlight == LocalSettings.showPostCommunityFirst), + ThunderToggleOption( + title: l10n.showPostCommunityIcons, + value: showCommunityIcons, + iconEnabled: Icons.groups, + iconDisabled: Icons.groups, + onChanged: (bool value) => setPreferences(LocalSettings.showPostCommunityIcons, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostCommunityIcons), + highlighted: settingToHighlight == LocalSettings.showPostCommunityIcons), + ThunderToggleOption( + title: l10n.showPostAuthor, + subtitle: l10n.showPostAuthorSubtitle, + value: showPostAuthor, + iconEnabled: Icons.person_rounded, + iconDisabled: Icons.person_off_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.showPostAuthor, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostAuthor), + highlighted: settingToHighlight == LocalSettings.showPostAuthor), + ThunderToggleOption( + title: l10n.showUserInstance, + value: postShowUserInstance, + iconEnabled: Icons.dns_sharp, + iconDisabled: Icons.dns_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.postShowUserInstance, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.postShowUserInstance), + highlighted: settingToHighlight == LocalSettings.postShowUserInstance), + ThunderToggleOption( + title: l10n.dimReadPosts, + subtitle: l10n.dimReadPosts, + value: dimReadPosts, + iconEnabled: Icons.chrome_reader_mode, + iconDisabled: Icons.chrome_reader_mode_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.dimReadPosts, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.dimReadPosts), + highlighted: settingToHighlight == LocalSettings.dimReadPosts), + ThunderToggleOption( + title: l10n.showFullDate, + subtitle: l10n.showFullDateDescription, + value: showFullPostDate, + iconEnabled: Icons.date_range_rounded, + iconDisabled: Icons.date_range_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.showFullPostDate, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showFullPostDate), + highlighted: settingToHighlight == LocalSettings.showFullPostDate), + ThunderListOption( + title: l10n.dateFormat, + disabled: !showFullPostDate, + value: ListPickerItem( + label: (selectedDateFormat == null || selectedDateFormat!.pattern == dateFormats.first.pattern) ? l10n.system : selectedDateFormat!.pattern!, + icon: Icons.access_time_filled_rounded, + payload: selectedDateFormat, + capitalizeLabel: false, + ), + options: dateFormats + .map( + (DateFormat dateFormat) => ListPickerItem( + icon: Icons.access_time_filled_rounded, + label: dateFormat.format(DateTime.now()), + payload: dateFormat, + subtitle: dateFormat.pattern == dateFormats.first.pattern ? l10n.system : dateFormat.pattern, + ), + ) + .toList(), + leading: Icon(Icons.access_time_filled_rounded), + onChanged: (value) async => setPreferences(LocalSettings.dateFormat, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.dateFormat), + highlighted: settingToHighlight == LocalSettings.dateFormat), + ThunderListOption( + title: l10n.dividerAppearance, + value: const ListPickerItem(payload: -1), + options: const [], + leading: Icon(Icons.splitscreen_rounded), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.dividerAppearance), + highlighted: settingToHighlight == LocalSettings.dividerAppearance, + customListPicker: StatefulBuilder( + builder: (context, setState) { + return BottomSheetListPicker( + title: l10n.dividerAppearance, + heading: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(l10n.preview, style: theme.textTheme.titleMedium), + const SizedBox(height: 20.0), + const FeedCardDivider(), + const SizedBox(height: 16.0), + ], + ), + items: [ + ListPickerItem( + customWidget: ListTile( + title: Text(l10n.thickness), + contentPadding: const EdgeInsets.only(left: 24.0, right: 20.0), + trailing: DropdownButton( + value: feedCardDividerThickness, + underline: const SizedBox(), + items: FeedCardDividerThickness.values.map((e) => DropdownMenuItem(value: e, child: Text(e.label))).toList(), + onChanged: (FeedCardDividerThickness? value) { + setPreferences(LocalSettings.feedCardDividerThickness, value); + setState(() {}); // Trigger rebuild + }, + ), ), + payload: -1, ), - payload: -1, - ), - ListPickerItem( - customWidget: ListTile( - title: Text(l10n.color), - contentPadding: const EdgeInsets.only(left: 24.0, right: 20.0), - trailing: DropdownButton( - menuMaxHeight: 500.0, - value: feedCardDividerColor, - underline: const SizedBox(), - items: CustomThemeType.values - .map((CustomThemeType customThemeType) => DropdownMenuItem( - alignment: Alignment.center, - value: Color(customThemeType.primaryColor.toARGB32()), - child: CircleAvatar( - radius: 16.0, - backgroundColor: Color.alphaBlend( - theme.colorScheme.primaryContainer.withValues(alpha: 0.6), - Color(customThemeType.primaryColor.toARGB32()), + ListPickerItem( + customWidget: ListTile( + title: Text(l10n.color), + contentPadding: const EdgeInsets.only(left: 24.0, right: 20.0), + trailing: DropdownButton( + menuMaxHeight: 500.0, + value: feedCardDividerColor, + underline: const SizedBox(), + items: CustomThemeType.values + .map((CustomThemeType customThemeType) => DropdownMenuItem( + alignment: Alignment.center, + value: Color(customThemeType.primaryColor.toARGB32()), + child: CircleAvatar( + radius: 16.0, + backgroundColor: Color.alphaBlend( + theme.colorScheme.primaryContainer.withValues(alpha: 0.6), + Color(customThemeType.primaryColor.toARGB32()), + ), ), - ), - )) - .toList() - ..insert( - 0, - const DropdownMenuItem( - alignment: Alignment.center, - value: null, - child: CircleAvatar(radius: 16.0, child: Text('D')), - ), - ) - ..insert( - 1, - const DropdownMenuItem( - alignment: Alignment.center, - value: Colors.transparent, - child: CircleAvatar(radius: 16.0, child: Text('T')), + )) + .toList() + ..insert( + 0, + const DropdownMenuItem( + alignment: Alignment.center, + value: null, + child: CircleAvatar(radius: 16.0, child: Text('D')), + ), + ) + ..insert( + 1, + const DropdownMenuItem( + alignment: Alignment.center, + value: Colors.transparent, + child: CircleAvatar(radius: 16.0, child: Text('T')), + ), ), - ), - onChanged: (Color? value) { - setPreferences(LocalSettings.feedCardDividerColor, value); - setState(() {}); // Trigger rebuild - }, + onChanged: (Color? value) { + setPreferences(LocalSettings.feedCardDividerColor, value); + setState(() {}); // Trigger rebuild + }, + ), ), + payload: -1, ), - payload: -1, - ), - ], - ); - }, - ), - ), + ], + ); + }, + )), SizedBox(height: 32.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), @@ -864,26 +853,24 @@ class _PostAppearanceSettingsPageState extends State ), ), SizedBox(height: 8.0), - ToggleOption( - description: l10n.showThumbnailPreviewOnRight, - value: showThumbnailPreviewOnRight, - iconEnabled: Icons.switch_left_rounded, - iconDisabled: Icons.switch_right_rounded, - onToggle: useCompactView == false ? null : (bool value) => setPreferences(LocalSettings.showThumbnailPreviewOnRight, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showThumbnailPreviewOnRight, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showTextPostIndicator, - value: showTextPostIndicator, - iconEnabled: Icons.article, - iconDisabled: Icons.article_outlined, - onToggle: useCompactView == false ? null : (bool value) => setPreferences(LocalSettings.showTextPostIndicator, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showTextPostIndicator, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.showThumbnailPreviewOnRight, + value: showThumbnailPreviewOnRight, + iconEnabled: Icons.switch_left_rounded, + iconDisabled: Icons.switch_right_rounded, + onChanged: useCompactView == false ? null : (bool value) => setPreferences(LocalSettings.showThumbnailPreviewOnRight, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showThumbnailPreviewOnRight), + highlighted: settingToHighlight == LocalSettings.showThumbnailPreviewOnRight), + ThunderToggleOption( + title: l10n.showTextPostIndicator, + value: showTextPostIndicator, + iconEnabled: Icons.article, + iconDisabled: Icons.article_outlined, + onChanged: useCompactView == false ? null : (bool value) => setPreferences(LocalSettings.showTextPostIndicator, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showTextPostIndicator), + highlighted: settingToHighlight == LocalSettings.showTextPostIndicator), SizedBox(height: 32.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), @@ -938,86 +925,78 @@ class _PostAppearanceSettingsPageState extends State ), ), SizedBox(height: 8.0), - ToggleOption( - description: l10n.showPostTitleFirst, - value: showTitleFirst, - iconEnabled: Icons.vertical_align_top_rounded, - iconDisabled: Icons.vertical_align_bottom_rounded, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostTitleFirst, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostTitleFirst, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showFullHeightImages, - value: showFullHeightImages, - iconEnabled: Icons.image_rounded, - iconDisabled: Icons.image_outlined, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostFullHeightImages, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostFullHeightImages, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showEdgeToEdgeImages, - value: showEdgeToEdgeImages, - iconEnabled: Icons.fit_screen_rounded, - iconDisabled: Icons.fit_screen_outlined, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostEdgeToEdgeImages, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostEdgeToEdgeImages, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostTextContentPreview, - value: showTextContent, - iconEnabled: Icons.notes_rounded, - iconDisabled: Icons.notes_rounded, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostTextContentPreview, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostTextContentPreview, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostVoteActions, - value: showVoteActions, - iconEnabled: Icons.import_export_rounded, - iconDisabled: Icons.import_export_rounded, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostVoteActions, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostVoteActions, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostSaveAction, - value: showSaveAction, - iconEnabled: Icons.star_rounded, - iconDisabled: Icons.star_border_rounded, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostSaveAction, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showPostSaveAction, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.linkPostsUseCompactView, - value: linkPostsUseCompactView, - iconEnabled: Icons.add_link, - iconDisabled: Icons.link, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.linkPostsUseCompactView, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.linkPostsUseCompactView, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.pinnedPostsUseCompactView, - value: pinnedPostsUseCompactView, - iconEnabled: Icons.push_pin, - iconDisabled: Icons.push_pin_outlined, - onToggle: useCompactView ? null : (bool value) => setPreferences(LocalSettings.pinnedPostsUseCompactView, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.pinnedPostsUseCompactView, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.showPostTitleFirst, + value: showTitleFirst, + iconEnabled: Icons.vertical_align_top_rounded, + iconDisabled: Icons.vertical_align_bottom_rounded, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostTitleFirst, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostTitleFirst), + highlighted: settingToHighlight == LocalSettings.showPostTitleFirst), + ThunderToggleOption( + title: l10n.showFullHeightImages, + value: showFullHeightImages, + iconEnabled: Icons.image_rounded, + iconDisabled: Icons.image_outlined, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostFullHeightImages, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostFullHeightImages), + highlighted: settingToHighlight == LocalSettings.showPostFullHeightImages), + ThunderToggleOption( + title: l10n.showEdgeToEdgeImages, + value: showEdgeToEdgeImages, + iconEnabled: Icons.fit_screen_rounded, + iconDisabled: Icons.fit_screen_outlined, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostEdgeToEdgeImages, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostEdgeToEdgeImages), + highlighted: settingToHighlight == LocalSettings.showPostEdgeToEdgeImages), + ThunderToggleOption( + title: l10n.showPostTextContentPreview, + value: showTextContent, + iconEnabled: Icons.notes_rounded, + iconDisabled: Icons.notes_rounded, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostTextContentPreview, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostTextContentPreview), + highlighted: settingToHighlight == LocalSettings.showPostTextContentPreview), + ThunderToggleOption( + title: l10n.showPostVoteActions, + value: showVoteActions, + iconEnabled: Icons.import_export_rounded, + iconDisabled: Icons.import_export_rounded, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostVoteActions, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostVoteActions), + highlighted: settingToHighlight == LocalSettings.showPostVoteActions), + ThunderToggleOption( + title: l10n.showPostSaveAction, + value: showSaveAction, + iconEnabled: Icons.star_rounded, + iconDisabled: Icons.star_border_rounded, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.showPostSaveAction, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showPostSaveAction), + highlighted: settingToHighlight == LocalSettings.showPostSaveAction), + ThunderToggleOption( + title: l10n.linkPostsUseCompactView, + value: linkPostsUseCompactView, + iconEnabled: Icons.add_link, + iconDisabled: Icons.link, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.linkPostsUseCompactView, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.linkPostsUseCompactView), + highlighted: settingToHighlight == LocalSettings.linkPostsUseCompactView), + ThunderToggleOption( + title: l10n.pinnedPostsUseCompactView, + value: pinnedPostsUseCompactView, + iconEnabled: Icons.push_pin, + iconDisabled: Icons.push_pin_outlined, + onChanged: useCompactView ? null : (bool value) => setPreferences(LocalSettings.pinnedPostsUseCompactView, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.pinnedPostsUseCompactView), + highlighted: settingToHighlight == LocalSettings.pinnedPostsUseCompactView), SizedBox(height: 32.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), @@ -1034,66 +1013,61 @@ class _PostAppearanceSettingsPageState extends State ], ), ), - ToggleOption( - description: l10n.showCrossPosts, - value: showCrossPosts, - iconEnabled: Icons.repeat_on_rounded, - iconDisabled: Icons.repeat_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.showCrossPosts, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showCrossPosts, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.postBodyViewType, - value: ListPickerItem( - label: switch (postBodyViewType) { - PostBodyViewType.condensed => l10n.condensed, - PostBodyViewType.expanded => l10n.expanded, - }, - icon: Icons.crop_16_9_rounded, - payload: postBodyViewType, - capitalizeLabel: false), - options: [ - ListPickerItem(icon: Icons.crop_16_9_rounded, label: l10n.condensed, payload: PostBodyViewType.condensed), - ListPickerItem(icon: Icons.crop_din_rounded, label: l10n.expanded, payload: PostBodyViewType.expanded), - ], - icon: Icons.view_list_rounded, - onChanged: (value) async => setPreferences(LocalSettings.postBodyViewType, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postBodyViewType, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showUserInstance, - value: postBodyShowUserInstance, - iconEnabled: Icons.dns_sharp, - iconDisabled: Icons.dns_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.postBodyShowUserInstance, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postBodyShowUserInstance, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.postBodyShowCommunityInstance, - value: postBodyShowCommunityInstance, - iconEnabled: Icons.dns_sharp, - iconDisabled: Icons.dns_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.postBodyShowCommunityInstance, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postBodyShowCommunityInstance, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showPostCommunityIcons, - value: postBodyShowCommunityAvatar, - iconEnabled: Icons.groups, - iconDisabled: Icons.groups, - onToggle: (bool value) => setPreferences(LocalSettings.postBodyShowCommunityAvatar, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postBodyShowCommunityAvatar, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.showCrossPosts, + value: showCrossPosts, + iconEnabled: Icons.repeat_on_rounded, + iconDisabled: Icons.repeat_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.showCrossPosts, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showCrossPosts), + highlighted: settingToHighlight == LocalSettings.showCrossPosts), + ThunderListOption( + title: l10n.postBodyViewType, + value: ListPickerItem( + label: switch (postBodyViewType) { + PostBodyViewType.condensed => l10n.condensed, + PostBodyViewType.expanded => l10n.expanded, + }, + icon: Icons.crop_16_9_rounded, + payload: postBodyViewType, + capitalizeLabel: false), + options: [ + ListPickerItem(icon: Icons.crop_16_9_rounded, label: l10n.condensed, payload: PostBodyViewType.condensed), + ListPickerItem(icon: Icons.crop_din_rounded, label: l10n.expanded, payload: PostBodyViewType.expanded), + ], + leading: Icon(Icons.view_list_rounded), + onChanged: (value) async => setPreferences(LocalSettings.postBodyViewType, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.postBodyViewType), + highlighted: settingToHighlight == LocalSettings.postBodyViewType), + ThunderToggleOption( + title: l10n.showUserInstance, + value: postBodyShowUserInstance, + iconEnabled: Icons.dns_sharp, + iconDisabled: Icons.dns_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.postBodyShowUserInstance, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.postBodyShowUserInstance), + highlighted: settingToHighlight == LocalSettings.postBodyShowUserInstance), + ThunderToggleOption( + title: l10n.postBodyShowCommunityInstance, + value: postBodyShowCommunityInstance, + iconEnabled: Icons.dns_sharp, + iconDisabled: Icons.dns_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.postBodyShowCommunityInstance, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.postBodyShowCommunityInstance), + highlighted: settingToHighlight == LocalSettings.postBodyShowCommunityInstance), + ThunderToggleOption( + title: l10n.showPostCommunityIcons, + value: postBodyShowCommunityAvatar, + iconEnabled: Icons.groups, + iconDisabled: Icons.groups, + onChanged: (bool value) => setPreferences(LocalSettings.postBodyShowCommunityAvatar, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.postBodyShowCommunityAvatar), + highlighted: settingToHighlight == LocalSettings.postBodyShowCommunityAvatar), SizedBox(height: 128.0), ], ), diff --git a/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart b/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart index f64429beb..cea0d1358 100644 --- a/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/appearance/theme_settings_page.dart @@ -14,7 +14,72 @@ import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; -import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, FullNameSeparator, ListPickerItem, NameColor, NameThickness; +import 'package:thunder/packages/ui/ui.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart' show CommunityFullNameWidget, UserFullNameWidget; + +String _generateSampleUserFullName(FullNameSeparator separator, bool useDisplayName) => generateUserFullName( + null, + 'name', + 'name', + 'instance.tld', + userSeparator: separator, + useDisplayName: useDisplayName, + ); + +Widget _generateSampleUserFullNameWidget( + FullNameSeparator separator, { + NameThickness? userNameThickness, + NameColor? userNameColor, + NameThickness? instanceNameThickness, + NameColor? instanceNameColor, + TextStyle? textStyle, + bool? useDisplayName, +}) => + UserFullNameWidget( + name: 'name', + displayName: 'name', + instance: 'instance.tld', + userSeparator: separator, + useDisplayName: useDisplayName ?? false, + userNameThickness: userNameThickness ?? NameThickness.normal, + userNameColor: userNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), + instanceNameThickness: instanceNameThickness ?? NameThickness.light, + instanceNameColor: instanceNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), + textStyle: textStyle, + fontScale: FontScale.base, + ); + +String _generateSampleCommunityFullName(FullNameSeparator separator, bool useDisplayName) => generateCommunityFullName( + null, + 'name', + 'name', + 'instance.tld', + communitySeparator: separator, + useDisplayName: useDisplayName, + ); + +Widget _generateSampleCommunityFullNameWidget( + FullNameSeparator separator, { + NameThickness? communityNameThickness, + NameColor? communityNameColor, + NameThickness? instanceNameThickness, + NameColor? instanceNameColor, + TextStyle? textStyle, + bool? useDisplayName, +}) => + CommunityFullNameWidget( + name: 'name', + displayName: 'name', + instance: 'instance.tld', + communitySeparator: separator, + useDisplayName: useDisplayName ?? false, + communityNameThickness: communityNameThickness ?? NameThickness.normal, + communityNameColor: communityNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), + instanceNameThickness: instanceNameThickness ?? NameThickness.light, + instanceNameColor: instanceNameColor ?? const NameColor.fromString(color: NameColor.defaultColor), + textStyle: textStyle, + fontScale: FontScale.base, + ); class ThemeSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -370,95 +435,91 @@ class _ThemeSettingsPageState extends State { padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), child: Text(l10n.theme, style: theme.textTheme.titleLarge), ), - ListOption( - description: l10n.theme, - value: ListPickerItem(label: themeType.name.capitalize, icon: Icons.wallpaper_rounded, payload: themeType), - options: themeOptions, - icon: Icons.wallpaper_rounded, - onChanged: (value) async => setPreferences(LocalSettings.appTheme, value.payload.index), - highlightKey: settingToHighlightKey, - setting: LocalSettings.appTheme, - highlightedSetting: settingToHighlight, - ), + ThunderListOption( + title: l10n.theme, + value: ListPickerItem(label: themeType.name.capitalize, icon: Icons.wallpaper_rounded, payload: themeType), + options: themeOptions, + leading: Icon(Icons.wallpaper_rounded), + onChanged: (value) async => setPreferences(LocalSettings.appTheme, value.payload.index), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.appTheme), + highlighted: settingToHighlight == LocalSettings.appTheme), AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeInOutCubicEmphasized, child: themeType == ThemeType.dark || themeType == ThemeType.system - ? ToggleOption( - description: l10n.pureBlack, + ? ThunderToggleOption( + title: l10n.pureBlack, subtitle: l10n.systemDarkModeDescription, value: usePureBlackTheme, iconEnabled: Icons.dark_mode_rounded, iconDisabled: Icons.dark_mode_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.usePureBlackTheme, value), + onChanged: (bool value) => setPreferences(LocalSettings.usePureBlackTheme, value), highlightKey: settingToHighlightKey, - setting: LocalSettings.usePureBlackTheme, - highlightedSetting: settingToHighlight, - ) + onLongPress: () => shareLocalSetting(context, LocalSettings.usePureBlackTheme), + highlighted: settingToHighlight == LocalSettings.usePureBlackTheme) : Container(), ), - ListOption( - description: l10n.themeAccentColor, - value: ListPickerItem(label: selectedTheme.label, icon: Icons.wallpaper_rounded, payload: selectedTheme), - valueDisplay: Stack( - children: [ - Container( - height: 28, - width: 28, - decoration: BoxDecoration( - color: selectedTheme.primaryColor, - borderRadius: BorderRadius.circular(100), - ), - ), - Positioned( - bottom: 0, - child: Container( - height: 14, - width: 14, + ThunderListOption( + title: l10n.themeAccentColor, + value: ListPickerItem(label: selectedTheme.label, icon: Icons.wallpaper_rounded, payload: selectedTheme), + valueDisplay: Stack( + children: [ + Container( + height: 28, + width: 28, decoration: BoxDecoration( - color: selectedTheme.secondaryColor, - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(100), + color: selectedTheme.primaryColor, + borderRadius: BorderRadius.circular(100), + ), + ), + Positioned( + bottom: 0, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: selectedTheme.secondaryColor, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(100), + ), ), ), ), - ), - Positioned( - bottom: 0, - right: 0, - child: Container( - height: 14, - width: 14, - decoration: BoxDecoration( - color: selectedTheme.tertiaryColor, - borderRadius: const BorderRadius.only( - bottomRight: Radius.circular(100), + Positioned( + bottom: 0, + right: 0, + child: Container( + height: 14, + width: 14, + decoration: BoxDecoration( + color: selectedTheme.tertiaryColor, + borderRadius: const BorderRadius.only( + bottomRight: Radius.circular(100), + ), ), ), ), - ), - ], - ), - options: customThemeOptions, - icon: Icons.wallpaper_rounded, - onChanged: (value) async => setPreferences(LocalSettings.appThemeAccentColor, value.payload), - closeOnSelect: false, - highlightKey: settingToHighlightKey, - setting: LocalSettings.appThemeAccentColor, - highlightedSetting: settingToHighlight, - ), - if (!kIsWeb && Platform.isAndroid) ...[ - ToggleOption( - description: l10n.useMaterialYouTheme, - subtitle: l10n.useMaterialYouThemeDescription, - value: useMaterialYouTheme, - iconEnabled: Icons.color_lens_rounded, - iconDisabled: Icons.color_lens_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.useMaterialYouTheme, value), + ], + ), + options: customThemeOptions, + leading: Icon(Icons.wallpaper_rounded), + onChanged: (value) async => setPreferences(LocalSettings.appThemeAccentColor, value.payload), + closeOnSelect: false, highlightKey: settingToHighlightKey, - setting: LocalSettings.useMaterialYouTheme, - highlightedSetting: settingToHighlight, - ) + onLongPress: () => shareLocalSetting(context, LocalSettings.appThemeAccentColor), + highlighted: settingToHighlight == LocalSettings.appThemeAccentColor), + if (!kIsWeb && Platform.isAndroid) ...[ + ThunderToggleOption( + title: l10n.useMaterialYouTheme, + subtitle: l10n.useMaterialYouThemeDescription, + value: useMaterialYouTheme, + iconEnabled: Icons.color_lens_rounded, + iconDisabled: Icons.color_lens_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.useMaterialYouTheme, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.useMaterialYouTheme), + highlighted: settingToHighlight == LocalSettings.useMaterialYouTheme) ], ], ), @@ -484,46 +545,42 @@ class _ThemeSettingsPageState extends State { padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), child: Text(l10n.fonts, style: theme.textTheme.titleLarge), ), - ListOption( - description: l10n.postTitleFontScale, - value: ListPickerItem(label: titleFontSizeScale.name.capitalize, icon: Icons.feed, payload: titleFontSizeScale), - options: fontScaleOptions, - icon: Icons.text_fields_rounded, - onChanged: (value) async => setPreferences(LocalSettings.titleFontSizeScale, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.titleFontSizeScale, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.postContentFontScale, - value: ListPickerItem(label: contentFontSizeScale.name.capitalize, icon: Icons.feed, payload: contentFontSizeScale), - options: fontScaleOptions, - icon: Icons.text_fields_rounded, - onChanged: (value) async => setPreferences(LocalSettings.contentFontSizeScale, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.contentFontSizeScale, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.commentFontScale, - value: ListPickerItem(label: commentFontSizeScale.name.capitalize, icon: Icons.feed, payload: commentFontSizeScale), - options: fontScaleOptions, - icon: Icons.text_fields_rounded, - onChanged: (value) async => setPreferences(LocalSettings.commentFontSizeScale, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.commentFontSizeScale, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.metadataFontScale, - value: ListPickerItem(label: metadataFontSizeScale.name.capitalize, icon: Icons.feed, payload: metadataFontSizeScale), - options: fontScaleOptions, - icon: Icons.text_fields_rounded, - onChanged: (value) async => setPreferences(LocalSettings.metadataFontSizeScale, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.metadataFontSizeScale, - highlightedSetting: settingToHighlight, - ), + ThunderListOption( + title: l10n.postTitleFontScale, + value: ListPickerItem(label: titleFontSizeScale.name.capitalize, icon: Icons.feed, payload: titleFontSizeScale), + options: fontScaleOptions, + leading: Icon(Icons.text_fields_rounded), + onChanged: (value) async => setPreferences(LocalSettings.titleFontSizeScale, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.titleFontSizeScale), + highlighted: settingToHighlight == LocalSettings.titleFontSizeScale), + ThunderListOption( + title: l10n.postContentFontScale, + value: ListPickerItem(label: contentFontSizeScale.name.capitalize, icon: Icons.feed, payload: contentFontSizeScale), + options: fontScaleOptions, + leading: Icon(Icons.text_fields_rounded), + onChanged: (value) async => setPreferences(LocalSettings.contentFontSizeScale, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.contentFontSizeScale), + highlighted: settingToHighlight == LocalSettings.contentFontSizeScale), + ThunderListOption( + title: l10n.commentFontScale, + value: ListPickerItem(label: commentFontSizeScale.name.capitalize, icon: Icons.feed, payload: commentFontSizeScale), + options: fontScaleOptions, + leading: Icon(Icons.text_fields_rounded), + onChanged: (value) async => setPreferences(LocalSettings.commentFontSizeScale, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.commentFontSizeScale), + highlighted: settingToHighlight == LocalSettings.commentFontSizeScale), + ThunderListOption( + title: l10n.metadataFontScale, + value: ListPickerItem(label: metadataFontSizeScale.name.capitalize, icon: Icons.feed, payload: metadataFontSizeScale), + options: fontScaleOptions, + leading: Icon(Icons.text_fields_rounded), + onChanged: (value) async => setPreferences(LocalSettings.metadataFontSizeScale, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.metadataFontSizeScale), + highlighted: settingToHighlight == LocalSettings.metadataFontSizeScale), ], ), ), @@ -537,29 +594,12 @@ class _ThemeSettingsPageState extends State { padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), child: Text(l10n.names, style: theme.textTheme.titleLarge), ), - ListOption( - description: l10n.userFormat, - value: ListPickerItem( - label: generateSampleUserFullName(userSeparator, useDisplayNamesForUsers), - labelWidget: generateSampleUserFullNameWidget( - userSeparator, - userNameThickness: userFullNameUserNameThickness, - userNameColor: userFullNameUserNameColor, - instanceNameThickness: userFullNameInstanceNameThickness, - instanceNameColor: userFullNameInstanceNameColor, - textStyle: theme.textTheme.bodyMedium, - useDisplayName: useDisplayNamesForUsers, - ), - icon: Icons.person_rounded, - payload: userSeparator, - capitalizeLabel: false, - ), - options: [ - ListPickerItem( - icon: const IconData(0x2022), - label: generateSampleUserFullName(FullNameSeparator.dot, useDisplayNamesForUsers), - labelWidget: generateSampleUserFullNameWidget( - FullNameSeparator.dot, + ThunderListOption( + title: l10n.userFormat, + value: ListPickerItem( + label: _generateSampleUserFullName(userSeparator, useDisplayNamesForUsers), + labelWidget: _generateSampleUserFullNameWidget( + userSeparator, userNameThickness: userFullNameUserNameThickness, userNameColor: userFullNameUserNameColor, instanceNameThickness: userFullNameInstanceNameThickness, @@ -567,60 +607,46 @@ class _ThemeSettingsPageState extends State { textStyle: theme.textTheme.bodyMedium, useDisplayName: useDisplayNamesForUsers, ), - payload: FullNameSeparator.dot, + icon: Icons.person_rounded, + payload: userSeparator, capitalizeLabel: false, ), - ListPickerItem( - icon: Icons.alternate_email_rounded, - label: generateSampleUserFullName(FullNameSeparator.at, useDisplayNamesForUsers), - labelWidget: generateSampleUserFullNameWidget( - FullNameSeparator.at, - userNameThickness: userFullNameUserNameThickness, - userNameColor: userFullNameUserNameColor, - instanceNameThickness: userFullNameInstanceNameThickness, - instanceNameColor: userFullNameInstanceNameColor, - textStyle: theme.textTheme.bodyMedium, - useDisplayName: useDisplayNamesForUsers, + options: [ + ListPickerItem( + icon: const IconData(0x2022), + label: _generateSampleUserFullName(FullNameSeparator.dot, useDisplayNamesForUsers), + labelWidget: _generateSampleUserFullNameWidget( + FullNameSeparator.dot, + userNameThickness: userFullNameUserNameThickness, + userNameColor: userFullNameUserNameColor, + instanceNameThickness: userFullNameInstanceNameThickness, + instanceNameColor: userFullNameInstanceNameColor, + textStyle: theme.textTheme.bodyMedium, + useDisplayName: useDisplayNamesForUsers, + ), + payload: FullNameSeparator.dot, + capitalizeLabel: false, ), - payload: FullNameSeparator.at, - capitalizeLabel: false, - ), - ListPickerItem( - icon: Icons.alternate_email_rounded, - label: generateSampleUserFullName(FullNameSeparator.lemmy, useDisplayNamesForUsers), - labelWidget: generateSampleUserFullNameWidget( - FullNameSeparator.lemmy, - userNameThickness: userFullNameUserNameThickness, - userNameColor: userFullNameUserNameColor, - instanceNameThickness: userFullNameInstanceNameThickness, - instanceNameColor: userFullNameInstanceNameColor, - textStyle: theme.textTheme.bodyMedium, - useDisplayName: useDisplayNamesForUsers, + ListPickerItem( + icon: Icons.alternate_email_rounded, + label: _generateSampleUserFullName(FullNameSeparator.at, useDisplayNamesForUsers), + labelWidget: _generateSampleUserFullNameWidget( + FullNameSeparator.at, + userNameThickness: userFullNameUserNameThickness, + userNameColor: userFullNameUserNameColor, + instanceNameThickness: userFullNameInstanceNameThickness, + instanceNameColor: userFullNameInstanceNameColor, + textStyle: theme.textTheme.bodyMedium, + useDisplayName: useDisplayNamesForUsers, + ), + payload: FullNameSeparator.at, + capitalizeLabel: false, ), - payload: FullNameSeparator.lemmy, - capitalizeLabel: false, - ), - ], - icon: Icons.person_rounded, - onChanged: (value) => setPreferences(LocalSettings.userFormat, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.userFormat, - highlightedSetting: settingToHighlight, - ), - ListOption( - isBottomModalScrollControlled: true, - value: const ListPickerItem(payload: -1), - description: l10n.userStyle, - icon: Icons.person_rounded, - highlightKey: settingToHighlightKey, - setting: LocalSettings.userStyle, - highlightedSetting: settingToHighlight, - customListPicker: StatefulBuilder( - builder: (context, setState) { - return BottomSheetListPicker( - title: l10n.userStyle, - heading: generateSampleUserFullNameWidget( - userSeparator, + ListPickerItem( + icon: Icons.alternate_email_rounded, + label: _generateSampleUserFullName(FullNameSeparator.lemmy, useDisplayNamesForUsers), + labelWidget: _generateSampleUserFullNameWidget( + FullNameSeparator.lemmy, userNameThickness: userFullNameUserNameThickness, userNameColor: userFullNameUserNameColor, instanceNameThickness: userFullNameInstanceNameThickness, @@ -628,165 +654,178 @@ class _ThemeSettingsPageState extends State { textStyle: theme.textTheme.bodyMedium, useDisplayName: useDisplayNamesForUsers, ), - items: [ - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.userNameThickness, - style: theme.textTheme.bodyMedium, - ), - subtitle: SizedBox( - width: 200.0, - child: Slider( - value: userFullNameUserNameThickness.toSliderValue(), - max: 2, - divisions: 2, - label: userFullNameUserNameThickness.label(context), - onChanged: (double value) async { - await setPreferences(LocalSettings.userFullNameUserNameThickness, NameThickness.fromSliderValue(value).name); - setState(() {}); - }, + payload: FullNameSeparator.lemmy, + capitalizeLabel: false, + ), + ], + leading: Icon(Icons.person_rounded), + onChanged: (value) => setPreferences(LocalSettings.userFormat, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.userFormat), + highlighted: settingToHighlight == LocalSettings.userFormat), + ThunderListOption( + isBottomModalScrollControlled: true, + value: const ListPickerItem(payload: -1), + options: const [], + title: l10n.userStyle, + leading: Icon(Icons.person_rounded), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.userStyle), + highlighted: settingToHighlight == LocalSettings.userStyle, + customListPicker: StatefulBuilder( + builder: (context, setState) { + return BottomSheetListPicker( + title: l10n.userStyle, + heading: _generateSampleUserFullNameWidget( + userSeparator, + userNameThickness: userFullNameUserNameThickness, + userNameColor: userFullNameUserNameColor, + instanceNameThickness: userFullNameInstanceNameThickness, + instanceNameColor: userFullNameInstanceNameColor, + textStyle: theme.textTheme.bodyMedium, + useDisplayName: useDisplayNamesForUsers, + ), + items: [ + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.userNameThickness, + style: theme.textTheme.bodyMedium, + ), + subtitle: SizedBox( + width: 200.0, + child: Slider( + value: userFullNameUserNameThickness.toSliderValue(), + max: 2, + divisions: 2, + label: userFullNameUserNameThickness.label(context), + onChanged: (double value) async { + await setPreferences(LocalSettings.userFullNameUserNameThickness, NameThickness.fromSliderValue(value).name); + setState(() {}); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.instanceNameThickness, - style: theme.textTheme.bodyMedium, - ), - subtitle: SizedBox( - width: 200.0, - child: Slider( - value: userFullNameInstanceNameThickness.toSliderValue(), - max: 2, - divisions: 2, - label: userFullNameInstanceNameThickness.label(context), - onChanged: (double value) async { - await setPreferences(LocalSettings.userFullNameInstanceNameThickness, NameThickness.fromSliderValue(value).name); - setState(() {}); - }, + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.instanceNameThickness, + style: theme.textTheme.bodyMedium, + ), + subtitle: SizedBox( + width: 200.0, + child: Slider( + value: userFullNameInstanceNameThickness.toSliderValue(), + max: 2, + divisions: 2, + label: userFullNameInstanceNameThickness.label(context), + onChanged: (double value) async { + await setPreferences(LocalSettings.userFullNameInstanceNameThickness, NameThickness.fromSliderValue(value).name); + setState(() {}); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.userNameColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: userFullNameUserNameColor, - items: NameColor.getPossibleValues(userFullNameUserNameColor) - .map( - (nameColor) => DropdownMenuItem( - alignment: Alignment.center, - value: nameColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: nameColor.toColor(context), - ), - const SizedBox(width: 16.0), - Text( - nameColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.userNameColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: userFullNameUserNameColor, + items: NameColor.getPossibleValues(userFullNameUserNameColor) + .map( + (nameColor) => DropdownMenuItem( + alignment: Alignment.center, + value: nameColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: nameColor.toColor(context), + ), + const SizedBox(width: 16.0), + Text( + nameColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.userFullNameUserNameColor, value?.color); - setState(() {}); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.userFullNameUserNameColor, value?.color); + setState(() {}); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.instanceNameColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: userFullNameInstanceNameColor, - items: NameColor.getPossibleValues(userFullNameInstanceNameColor) - .map( - (nameColor) => DropdownMenuItem( - alignment: Alignment.center, - value: nameColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: nameColor.toColor(context), - ), - const SizedBox(width: 16.0), - Text( - nameColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.instanceNameColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: userFullNameInstanceNameColor, + items: NameColor.getPossibleValues(userFullNameInstanceNameColor) + .map( + (nameColor) => DropdownMenuItem( + alignment: Alignment.center, + value: nameColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: nameColor.toColor(context), + ), + const SizedBox(width: 16.0), + Text( + nameColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.userFullNameInstanceNameColor, value?.color); - setState(() {}); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.userFullNameInstanceNameColor, value?.color); + setState(() {}); + }, + ), ), ), ), - ), - ], - ); - }, - ), - ), - ListOption( - description: l10n.communityFormat, - value: ListPickerItem( - label: generateSampleCommunityFullName(communitySeparator, useDisplayNamesForCommunities), - labelWidget: generateSampleCommunityFullNameWidget( - communitySeparator, - communityNameThickness: communityFullNameCommunityNameThickness, - communityNameColor: communityFullNameCommunityNameColor, - instanceNameThickness: communityFullNameInstanceNameThickness, - instanceNameColor: communityFullNameInstanceNameColor, - textStyle: theme.textTheme.bodyMedium, - useDisplayName: useDisplayNamesForCommunities, - ), - icon: Icons.people_rounded, - payload: communitySeparator, - capitalizeLabel: false, - ), - options: [ - ListPickerItem( - icon: const IconData(0x2022), - label: generateSampleCommunityFullName(FullNameSeparator.dot, useDisplayNamesForCommunities), - labelWidget: generateSampleCommunityFullNameWidget( - FullNameSeparator.dot, + ], + ); + }, + )), + ThunderListOption( + title: l10n.communityFormat, + value: ListPickerItem( + label: _generateSampleCommunityFullName(communitySeparator, useDisplayNamesForCommunities), + labelWidget: _generateSampleCommunityFullNameWidget( + communitySeparator, communityNameThickness: communityFullNameCommunityNameThickness, communityNameColor: communityFullNameCommunityNameColor, instanceNameThickness: communityFullNameInstanceNameThickness, @@ -794,60 +833,46 @@ class _ThemeSettingsPageState extends State { textStyle: theme.textTheme.bodyMedium, useDisplayName: useDisplayNamesForCommunities, ), - payload: FullNameSeparator.dot, + icon: Icons.people_rounded, + payload: communitySeparator, capitalizeLabel: false, ), - ListPickerItem( - icon: Icons.alternate_email_rounded, - label: generateSampleCommunityFullName(FullNameSeparator.at, useDisplayNamesForCommunities), - labelWidget: generateSampleCommunityFullNameWidget( - FullNameSeparator.at, - communityNameThickness: communityFullNameCommunityNameThickness, - communityNameColor: communityFullNameCommunityNameColor, - instanceNameThickness: communityFullNameInstanceNameThickness, - instanceNameColor: communityFullNameInstanceNameColor, - textStyle: theme.textTheme.bodyMedium, - useDisplayName: useDisplayNamesForCommunities, + options: [ + ListPickerItem( + icon: const IconData(0x2022), + label: _generateSampleCommunityFullName(FullNameSeparator.dot, useDisplayNamesForCommunities), + labelWidget: _generateSampleCommunityFullNameWidget( + FullNameSeparator.dot, + communityNameThickness: communityFullNameCommunityNameThickness, + communityNameColor: communityFullNameCommunityNameColor, + instanceNameThickness: communityFullNameInstanceNameThickness, + instanceNameColor: communityFullNameInstanceNameColor, + textStyle: theme.textTheme.bodyMedium, + useDisplayName: useDisplayNamesForCommunities, + ), + payload: FullNameSeparator.dot, + capitalizeLabel: false, ), - payload: FullNameSeparator.at, - capitalizeLabel: false, - ), - ListPickerItem( - icon: Icons.alternate_email_rounded, - label: generateSampleCommunityFullName(FullNameSeparator.lemmy, useDisplayNamesForCommunities), - labelWidget: generateSampleCommunityFullNameWidget( - FullNameSeparator.lemmy, - communityNameThickness: communityFullNameCommunityNameThickness, - communityNameColor: communityFullNameCommunityNameColor, - instanceNameThickness: communityFullNameInstanceNameThickness, - instanceNameColor: communityFullNameInstanceNameColor, - textStyle: theme.textTheme.bodyMedium, - useDisplayName: useDisplayNamesForCommunities, + ListPickerItem( + icon: Icons.alternate_email_rounded, + label: _generateSampleCommunityFullName(FullNameSeparator.at, useDisplayNamesForCommunities), + labelWidget: _generateSampleCommunityFullNameWidget( + FullNameSeparator.at, + communityNameThickness: communityFullNameCommunityNameThickness, + communityNameColor: communityFullNameCommunityNameColor, + instanceNameThickness: communityFullNameInstanceNameThickness, + instanceNameColor: communityFullNameInstanceNameColor, + textStyle: theme.textTheme.bodyMedium, + useDisplayName: useDisplayNamesForCommunities, + ), + payload: FullNameSeparator.at, + capitalizeLabel: false, ), - payload: FullNameSeparator.lemmy, - capitalizeLabel: false, - ), - ], - icon: Icons.people_rounded, - onChanged: (value) => setPreferences(LocalSettings.communityFormat, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.communityFormat, - highlightedSetting: settingToHighlight, - ), - ListOption( - isBottomModalScrollControlled: true, - value: const ListPickerItem(payload: -1), - description: l10n.communityStyle, - icon: Icons.person_rounded, - highlightKey: settingToHighlightKey, - setting: LocalSettings.communityStyle, - highlightedSetting: settingToHighlight, - customListPicker: StatefulBuilder( - builder: (context, setState) { - return BottomSheetListPicker( - title: l10n.communityStyle, - heading: generateSampleCommunityFullNameWidget( - communitySeparator, + ListPickerItem( + icon: Icons.alternate_email_rounded, + label: _generateSampleCommunityFullName(FullNameSeparator.lemmy, useDisplayNamesForCommunities), + labelWidget: _generateSampleCommunityFullNameWidget( + FullNameSeparator.lemmy, communityNameThickness: communityFullNameCommunityNameThickness, communityNameColor: communityFullNameCommunityNameColor, instanceNameThickness: communityFullNameInstanceNameThickness, @@ -855,162 +880,190 @@ class _ThemeSettingsPageState extends State { textStyle: theme.textTheme.bodyMedium, useDisplayName: useDisplayNamesForCommunities, ), - items: [ - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.communityNameThickness, - style: theme.textTheme.bodyMedium, - ), - subtitle: SizedBox( - width: 200.0, - child: Slider( - value: communityFullNameCommunityNameThickness.toSliderValue(), - max: 2, - divisions: 2, - label: communityFullNameCommunityNameThickness.label(context), - onChanged: (double value) async { - await setPreferences(LocalSettings.communityFullNameCommunityNameThickness, NameThickness.fromSliderValue(value).name); - setState(() {}); - }, + payload: FullNameSeparator.lemmy, + capitalizeLabel: false, + ), + ], + leading: Icon(Icons.people_rounded), + onChanged: (value) => setPreferences(LocalSettings.communityFormat, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.communityFormat), + highlighted: settingToHighlight == LocalSettings.communityFormat), + ThunderListOption( + isBottomModalScrollControlled: true, + value: const ListPickerItem(payload: -1), + options: const [], + title: l10n.communityStyle, + leading: Icon(Icons.person_rounded), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.communityStyle), + highlighted: settingToHighlight == LocalSettings.communityStyle, + customListPicker: StatefulBuilder( + builder: (context, setState) { + return BottomSheetListPicker( + title: l10n.communityStyle, + heading: _generateSampleCommunityFullNameWidget( + communitySeparator, + communityNameThickness: communityFullNameCommunityNameThickness, + communityNameColor: communityFullNameCommunityNameColor, + instanceNameThickness: communityFullNameInstanceNameThickness, + instanceNameColor: communityFullNameInstanceNameColor, + textStyle: theme.textTheme.bodyMedium, + useDisplayName: useDisplayNamesForCommunities, + ), + items: [ + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.communityNameThickness, + style: theme.textTheme.bodyMedium, + ), + subtitle: SizedBox( + width: 200.0, + child: Slider( + value: communityFullNameCommunityNameThickness.toSliderValue(), + max: 2, + divisions: 2, + label: communityFullNameCommunityNameThickness.label(context), + onChanged: (double value) async { + await setPreferences(LocalSettings.communityFullNameCommunityNameThickness, NameThickness.fromSliderValue(value).name); + setState(() {}); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.instanceNameThickness, - style: theme.textTheme.bodyMedium, - ), - subtitle: SizedBox( - width: 200.0, - child: Slider( - value: communityFullNameInstanceNameThickness.toSliderValue(), - max: 2, - divisions: 2, - label: communityFullNameInstanceNameThickness.label(context), - onChanged: (double value) async { - await setPreferences(LocalSettings.communityFullNameInstanceNameThickness, NameThickness.fromSliderValue(value).name); - setState(() {}); - }, + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.instanceNameThickness, + style: theme.textTheme.bodyMedium, + ), + subtitle: SizedBox( + width: 200.0, + child: Slider( + value: communityFullNameInstanceNameThickness.toSliderValue(), + max: 2, + divisions: 2, + label: communityFullNameInstanceNameThickness.label(context), + onChanged: (double value) async { + await setPreferences(LocalSettings.communityFullNameInstanceNameThickness, NameThickness.fromSliderValue(value).name); + setState(() {}); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.communityNameColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: communityFullNameCommunityNameColor, - items: NameColor.getPossibleValues(communityFullNameCommunityNameColor) - .map( - (nameColor) => DropdownMenuItem( - alignment: Alignment.center, - value: nameColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: nameColor.toColor(context), - ), - const SizedBox(width: 16.0), - Text( - nameColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.communityNameColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: communityFullNameCommunityNameColor, + items: NameColor.getPossibleValues(communityFullNameCommunityNameColor) + .map( + (nameColor) => DropdownMenuItem( + alignment: Alignment.center, + value: nameColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: nameColor.toColor(context), + ), + const SizedBox(width: 16.0), + Text( + nameColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.communityFullNameCommunityNameColor, value?.color); - setState(() {}); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.communityFullNameCommunityNameColor, value?.color); + setState(() {}); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.instanceNameColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: communityFullNameInstanceNameColor, - items: NameColor.getPossibleValues(communityFullNameInstanceNameColor) - .map( - (nameColor) => DropdownMenuItem( - alignment: Alignment.center, - value: nameColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: nameColor.toColor(context), - ), - const SizedBox(width: 16.0), - Text( - nameColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.instanceNameColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: communityFullNameInstanceNameColor, + items: NameColor.getPossibleValues(communityFullNameInstanceNameColor) + .map( + (nameColor) => DropdownMenuItem( + alignment: Alignment.center, + value: nameColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: nameColor.toColor(context), + ), + const SizedBox(width: 16.0), + Text( + nameColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.communityFullNameInstanceNameColor, value?.color); - setState(() {}); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.communityFullNameInstanceNameColor, value?.color); + setState(() {}); + }, + ), ), ), ), - ), - ], - ); - }, - ), - ), - ToggleOption( - description: l10n.showUserDisplayNames, - value: useDisplayNamesForUsers, - iconEnabled: Icons.person_rounded, - iconDisabled: Icons.person_off_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.useDisplayNamesForUsers, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.useDisplayNamesForUsers, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showCommunityDisplayNames, - value: useDisplayNamesForCommunities, - iconEnabled: Icons.people_rounded, - iconDisabled: Icons.people_outline_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.useDisplayNamesForCommunities, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.useDisplayNamesForCommunities, - highlightedSetting: settingToHighlight, - ), + ], + ); + }, + )), + ThunderToggleOption( + title: l10n.showUserDisplayNames, + value: useDisplayNamesForUsers, + iconEnabled: Icons.person_rounded, + iconDisabled: Icons.person_off_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.useDisplayNamesForUsers, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.useDisplayNamesForUsers), + highlighted: settingToHighlight == LocalSettings.useDisplayNamesForUsers), + ThunderToggleOption( + title: l10n.showCommunityDisplayNames, + value: useDisplayNamesForCommunities, + iconEnabled: Icons.people_rounded, + iconDisabled: Icons.people_outline_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.useDisplayNamesForCommunities, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.useDisplayNamesForCommunities), + highlighted: settingToHighlight == LocalSettings.useDisplayNamesForCommunities), ], ), ), diff --git a/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart index 515953a85..8e0ce432b 100644 --- a/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/fab_settings_page.dart @@ -7,13 +7,13 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/features/settings/settings.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; -import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, ListPickerItem; +import 'package:thunder/packages/ui/ui.dart'; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; class FabSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -312,14 +312,13 @@ class _FabSettingsPage extends State with TickerProviderStateMi padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), child: Text(l10n.feed, style: theme.textTheme.titleMedium), ), - ToggleOption( - description: l10n.enableFeedFab, - value: enableFeedsFab, - onToggle: (bool value) => setPreferences(LocalSettings.enableFeedsFab, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableFeedsFab, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.enableFeedFab, + value: enableFeedsFab, + onChanged: (bool value) => setPreferences(LocalSettings.enableFeedsFab, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enableFeedsFab), + highlighted: settingToHighlight == LocalSettings.enableFeedsFab), AnimatedSwitcher( duration: const Duration(milliseconds: 250), switchInCurve: Curves.easeInOut, @@ -333,140 +332,126 @@ class _FabSettingsPage extends State with TickerProviderStateMi child: enableFeedsFab ? Column( children: [ - ToggleOption( - description: l10n.expandOptions, - value: null, - semanticLabel: """${l10n.expandOptions} + ThunderToggleOption( + title: l10n.expandOptions, + value: null, + semanticLabel: """${l10n.expandOptions} ${feedFabSinglePressAction == FeedFabAction.openFab ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.openFab ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.more_horiz_rounded, - iconDisabled: Icons.more_horiz_rounded, - onToggle: (_) {}, - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.openFab) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.openFab) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.openFab), - onTap: () => showFeedFabActionPicker(FeedFabAction.openFab), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.backToTop, - value: enableBackToTop, - semanticLabel: """${l10n.backToTop} + iconEnabled: Icons.more_horiz_rounded, + iconDisabled: Icons.more_horiz_rounded, + onChanged: (_) {}, + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.openFab) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.openFab) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.openFab), + onTap: () => showFeedFabActionPicker(FeedFabAction.openFab), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: false), + ThunderToggleOption( + title: l10n.backToTop, + value: enableBackToTop, + semanticLabel: """${l10n.backToTop} ${feedFabSinglePressAction == FeedFabAction.backToTop ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.backToTop ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.arrow_upward, - iconDisabled: Icons.arrow_upward, - onToggle: (bool value) => setPreferences(LocalSettings.enableBackToTop, value), - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.backToTop) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.backToTop) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.backToTop), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableBackToTop, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.subscriptions, - value: enableSubscriptions, - semanticLabel: """${l10n.subscriptions} + iconEnabled: Icons.arrow_upward, + iconDisabled: Icons.arrow_upward, + onChanged: (bool value) => setPreferences(LocalSettings.enableBackToTop, value), + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.backToTop) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.backToTop) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.backToTop), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.enableBackToTop), + ThunderToggleOption( + title: l10n.subscriptions, + value: enableSubscriptions, + semanticLabel: """${l10n.subscriptions} ${feedFabSinglePressAction == FeedFabAction.subscriptions ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.subscriptions ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.people_rounded, - iconDisabled: Icons.people_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.enableSubscriptions, value), - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.subscriptions) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.subscriptions) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.subscriptions), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableSubscriptions, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.changeSort, - value: enableChangeSort, - semanticLabel: """${l10n.changeSort} + iconEnabled: Icons.people_rounded, + iconDisabled: Icons.people_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.enableSubscriptions, value), + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.subscriptions) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.subscriptions) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.subscriptions), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.enableSubscriptions), + ThunderToggleOption( + title: l10n.changeSort, + value: enableChangeSort, + semanticLabel: """${l10n.changeSort} ${feedFabSinglePressAction == FeedFabAction.changeSort ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.changeSort ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.sort_rounded, - iconDisabled: Icons.sort_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.enableChangeSort, value), - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.changeSort) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.changeSort) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.changeSort), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableChangeSort, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.refresh, - value: enableRefresh, - semanticLabel: """${l10n.refresh} + iconEnabled: Icons.sort_rounded, + iconDisabled: Icons.sort_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.enableChangeSort, value), + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.changeSort) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.changeSort) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.changeSort), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.enableChangeSort), + ThunderToggleOption( + title: l10n.refresh, + value: enableRefresh, + semanticLabel: """${l10n.refresh} ${feedFabSinglePressAction == FeedFabAction.refresh ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.refresh ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.refresh_rounded, - iconDisabled: Icons.refresh_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.enableRefresh, value), - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.refresh) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.refresh) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.refresh), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableRefresh, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.dismissRead, - value: enableDismissRead, - semanticLabel: """${l10n.dismissRead} + iconEnabled: Icons.refresh_rounded, + iconDisabled: Icons.refresh_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.enableRefresh, value), + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.refresh) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.refresh) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.refresh), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.enableRefresh), + ThunderToggleOption( + title: l10n.dismissRead, + value: enableDismissRead, + semanticLabel: """${l10n.dismissRead} ${feedFabSinglePressAction == FeedFabAction.dismissRead ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.dismissRead ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.clear_all_rounded, - iconDisabled: Icons.clear_all_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.enableDismissRead, value), - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.dismissRead) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.dismissRead) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.dismissRead), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableDismissRead, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.createPost, - value: enableNewPost, - semanticLabel: """${l10n.createPost} + iconEnabled: Icons.clear_all_rounded, + iconDisabled: Icons.clear_all_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.enableDismissRead, value), + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.dismissRead) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.dismissRead) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.dismissRead), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.enableDismissRead), + ThunderToggleOption( + title: l10n.createPost, + value: enableNewPost, + semanticLabel: """${l10n.createPost} ${feedFabSinglePressAction == FeedFabAction.newPost ? l10n.currentSinglePress : ''} ${feedFabLongPressAction == FeedFabAction.newPost ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.add_rounded, - iconDisabled: Icons.add_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.enableNewPost, value), - additionalWidgets: [ - if (feedFabSinglePressAction == FeedFabAction.newPost) const Icon(Icons.touch_app_outlined), - if (feedFabLongPressAction == FeedFabAction.newPost) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showFeedFabActionPicker(FeedFabAction.newPost), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableNewPost, - highlightedSetting: settingToHighlight, - ), + iconEnabled: Icons.add_rounded, + iconDisabled: Icons.add_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.enableNewPost, value), + additionalTrailing: [ + if (feedFabSinglePressAction == FeedFabAction.newPost) const Icon(Icons.touch_app_outlined), + if (feedFabLongPressAction == FeedFabAction.newPost) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showFeedFabActionPicker(FeedFabAction.newPost), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.enableNewPost), ], ) : null, @@ -475,14 +460,13 @@ class _FabSettingsPage extends State with TickerProviderStateMi padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), child: Text(l10n.posts, style: theme.textTheme.titleMedium), ), - ToggleOption( - description: l10n.enablePostFab, - value: enablePostsFab, - onToggle: (bool value) => setPreferences(LocalSettings.enablePostsFab, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enablePostsFab, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.enablePostFab, + value: enablePostsFab, + onChanged: (bool value) => setPreferences(LocalSettings.enablePostsFab, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enablePostsFab), + highlighted: settingToHighlight == LocalSettings.enablePostsFab), AnimatedSwitcher( duration: const Duration(milliseconds: 250), switchInCurve: Curves.easeInOut, @@ -496,121 +480,109 @@ class _FabSettingsPage extends State with TickerProviderStateMi child: enablePostsFab ? Column( children: [ - ToggleOption( - description: l10n.expandOptions, - value: null, - semanticLabel: """${l10n.expandOptions} + ThunderToggleOption( + title: l10n.expandOptions, + value: null, + semanticLabel: """${l10n.expandOptions} ${postFabSinglePressAction == PostFabAction.openFab ? l10n.currentSinglePress : ''} ${postFabLongPressAction == PostFabAction.openFab ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.more_horiz_rounded, - iconDisabled: Icons.more_horiz_rounded, - onToggle: (_) {}, - additionalWidgets: [ - if (postFabSinglePressAction == PostFabAction.openFab) const Icon(Icons.touch_app_outlined), - if (postFabLongPressAction == PostFabAction.openFab) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showPostFabActionPicker(PostFabAction.openFab), - onTap: () => showPostFabActionPicker(PostFabAction.openFab), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.search, - value: postFabEnableSearch, - semanticLabel: """${l10n.search} + iconEnabled: Icons.more_horiz_rounded, + iconDisabled: Icons.more_horiz_rounded, + onChanged: (_) {}, + additionalTrailing: [ + if (postFabSinglePressAction == PostFabAction.openFab) const Icon(Icons.touch_app_outlined), + if (postFabLongPressAction == PostFabAction.openFab) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showPostFabActionPicker(PostFabAction.openFab), + onTap: () => showPostFabActionPicker(PostFabAction.openFab), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: false), + ThunderToggleOption( + title: l10n.search, + value: postFabEnableSearch, + semanticLabel: """${l10n.search} ${postFabSinglePressAction == PostFabAction.search ? l10n.currentSinglePress : ''} ${postFabLongPressAction == PostFabAction.search ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.search_rounded, - iconDisabled: Icons.search_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.postFabEnableSearch, value), - additionalWidgets: [ - if (postFabSinglePressAction == PostFabAction.search) const Icon(Icons.touch_app_outlined), - if (postFabLongPressAction == PostFabAction.search) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showPostFabActionPicker(PostFabAction.search), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postFabEnableSearch, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.backToTop, - value: postFabEnableBackToTop, - semanticLabel: """${l10n.backToTop} + iconEnabled: Icons.search_rounded, + iconDisabled: Icons.search_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.postFabEnableSearch, value), + additionalTrailing: [ + if (postFabSinglePressAction == PostFabAction.search) const Icon(Icons.touch_app_outlined), + if (postFabLongPressAction == PostFabAction.search) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showPostFabActionPicker(PostFabAction.search), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.postFabEnableSearch), + ThunderToggleOption( + title: l10n.backToTop, + value: postFabEnableBackToTop, + semanticLabel: """${l10n.backToTop} ${postFabSinglePressAction == PostFabAction.backToTop ? l10n.currentSinglePress : ''} ${postFabLongPressAction == PostFabAction.backToTop ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.arrow_upward, - iconDisabled: Icons.arrow_upward, - onToggle: (bool value) => setPreferences(LocalSettings.postFabEnableBackToTop, value), - additionalWidgets: [ - if (postFabSinglePressAction == PostFabAction.backToTop) const Icon(Icons.touch_app_outlined), - if (postFabLongPressAction == PostFabAction.backToTop) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showPostFabActionPicker(PostFabAction.backToTop), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postFabEnableBackToTop, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.changeSort, - value: postFabEnableChangeSort, - semanticLabel: """${l10n.changeSort} + iconEnabled: Icons.arrow_upward, + iconDisabled: Icons.arrow_upward, + onChanged: (bool value) => setPreferences(LocalSettings.postFabEnableBackToTop, value), + additionalTrailing: [ + if (postFabSinglePressAction == PostFabAction.backToTop) const Icon(Icons.touch_app_outlined), + if (postFabLongPressAction == PostFabAction.backToTop) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showPostFabActionPicker(PostFabAction.backToTop), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.postFabEnableBackToTop), + ThunderToggleOption( + title: l10n.changeSort, + value: postFabEnableChangeSort, + semanticLabel: """${l10n.changeSort} ${postFabSinglePressAction == PostFabAction.changeSort ? l10n.currentSinglePress : ''} ${postFabLongPressAction == PostFabAction.changeSort ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.sort_rounded, - iconDisabled: Icons.sort_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.postFabEnableChangeSort, value), - additionalWidgets: [ - if (postFabSinglePressAction == PostFabAction.changeSort) const Icon(Icons.touch_app_outlined), - if (postFabLongPressAction == PostFabAction.changeSort) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showPostFabActionPicker(PostFabAction.changeSort), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postFabEnableChangeSort, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.replyToPost, - value: postFabEnableReplyToPost, - semanticLabel: """${l10n.replyToPost} + iconEnabled: Icons.sort_rounded, + iconDisabled: Icons.sort_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.postFabEnableChangeSort, value), + additionalTrailing: [ + if (postFabSinglePressAction == PostFabAction.changeSort) const Icon(Icons.touch_app_outlined), + if (postFabLongPressAction == PostFabAction.changeSort) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showPostFabActionPicker(PostFabAction.changeSort), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.postFabEnableChangeSort), + ThunderToggleOption( + title: l10n.replyToPost, + value: postFabEnableReplyToPost, + semanticLabel: """${l10n.replyToPost} ${postFabSinglePressAction == PostFabAction.replyToPost ? l10n.currentSinglePress : ''} ${postFabLongPressAction == PostFabAction.replyToPost ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.reply_rounded, - iconDisabled: Icons.reply_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.postFabEnableReplyToPost, value), - additionalWidgets: [ - if (postFabSinglePressAction == PostFabAction.replyToPost) const Icon(Icons.touch_app_outlined), - if (postFabLongPressAction == PostFabAction.replyToPost) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showPostFabActionPicker(PostFabAction.replyToPost), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postFabEnableReplyToPost, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.refresh, - value: postFabEnableRefresh, - semanticLabel: """${l10n.refresh} + iconEnabled: Icons.reply_rounded, + iconDisabled: Icons.reply_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.postFabEnableReplyToPost, value), + additionalTrailing: [ + if (postFabSinglePressAction == PostFabAction.replyToPost) const Icon(Icons.touch_app_outlined), + if (postFabLongPressAction == PostFabAction.replyToPost) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showPostFabActionPicker(PostFabAction.replyToPost), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.postFabEnableReplyToPost), + ThunderToggleOption( + title: l10n.refresh, + value: postFabEnableRefresh, + semanticLabel: """${l10n.refresh} ${postFabSinglePressAction == PostFabAction.refresh ? l10n.currentSinglePress : ''} ${postFabLongPressAction == PostFabAction.refresh ? l10n.currentLongPress : ''}""", - iconEnabled: Icons.refresh_rounded, - iconDisabled: Icons.refresh_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.postFabEnableRefresh, value), - additionalWidgets: [ - if (postFabSinglePressAction == PostFabAction.refresh) const Icon(Icons.touch_app_outlined), - if (postFabLongPressAction == PostFabAction.refresh) const Icon(Icons.touch_app_rounded), - ], - onLongPress: () => showPostFabActionPicker(PostFabAction.refresh), - padding: const EdgeInsets.only(left: 24.0, right: 16.0), - highlightKey: settingToHighlightKey, - setting: LocalSettings.postFabEnableRefresh, - highlightedSetting: settingToHighlight, - ), + iconEnabled: Icons.refresh_rounded, + iconDisabled: Icons.refresh_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.postFabEnableRefresh, value), + additionalTrailing: [ + if (postFabSinglePressAction == PostFabAction.refresh) const Icon(Icons.touch_app_outlined), + if (postFabLongPressAction == PostFabAction.refresh) const Icon(Icons.touch_app_rounded), + ], + onLongPress: () => showPostFabActionPicker(PostFabAction.refresh), + padding: const EdgeInsets.only(left: 24.0, right: 16.0), + highlightKey: settingToHighlightKey, + highlighted: settingToHighlight == LocalSettings.postFabEnableRefresh), ], ) : null, diff --git a/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart index 6e46bf270..829877563 100644 --- a/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/filter_settings_page.dart @@ -8,7 +8,6 @@ import 'package:smooth_highlight/smooth_highlight.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; @@ -16,7 +15,7 @@ import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show showThunderDialog; +import 'package:thunder/packages/ui/ui.dart'; class FilterSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -157,56 +156,52 @@ class _FilterSettingsPageState extends State with SingleTick shrinkWrap: true, itemCount: keywordFilters.length, itemBuilder: (context, index) { - return SettingsListTile( - description: keywordFilters[index], - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), - onTap: () async { - showThunderDialog( - context: context, - title: l10n.removeKeywordFilter, - contentText: l10n.removeKeyword(keywordFilters[index]), - primaryButtonText: l10n.remove, - onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { - setPreferences(LocalSettings.keywordFilters, keywordFilters.where((element) => element != keywordFilters[index]).toList()); - Navigator.of(dialogContext).pop(); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - ); - }, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ); + return ThunderSettingsTile( + title: keywordFilters[index], + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + onTap: () async { + showThunderDialog( + context: context, + title: l10n.removeKeywordFilter, + contentText: l10n.removeKeyword(keywordFilters[index]), + primaryButtonText: l10n.remove, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { + setPreferences(LocalSettings.keywordFilters, keywordFilters.where((element) => element != keywordFilters[index]).toList()); + Navigator.of(dialogContext).pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + }, + highlightKey: settingToHighlightKey, + highlighted: false); }, ), ), SizedBox(height: 16.0), - SettingsListTile( - icon: Icons.language, - description: l10n.languageFilters, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () { - // Can only set discussion language if user is logged in - if (profileState.isLoggedIn && profileState.status == ProfileStatus.success && profileState.user != null) { - navigateToSettingPage(context, LocalSettings.settingsPageAccountLanguages); - } else { - showThunderDialog( - context: context, - title: l10n.userNotLoggedIn, - contentText: l10n.mustBeLoggedIn, - primaryButtonText: l10n.ok, - onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) => Navigator.of(dialogContext).pop(), - ); - } - }, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.language), + title: l10n.languageFilters, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () { + // Can only set discussion language if user is logged in + if (profileState.isLoggedIn && profileState.status == ProfileStatus.success && profileState.user != null) { + navigateToSettingPage(context, LocalSettings.settingsPageAccountLanguages); + } else { + showThunderDialog( + context: context, + title: l10n.userNotLoggedIn, + contentText: l10n.mustBeLoggedIn, + primaryButtonText: l10n.ok, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) => Navigator.of(dialogContext).pop(), + ); + } + }, + highlightKey: settingToHighlightKey, + highlighted: false), SizedBox(height: 128.0), ], ), diff --git a/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart index 9f60472b4..78f457659 100644 --- a/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/general_settings_page.dart @@ -16,8 +16,7 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/account.dart'; import 'package:thunder/src/features/notification/notification.dart'; -import 'package:thunder/src/features/settings/settings.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; import 'package:thunder/src/shared/sort_picker.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; @@ -26,7 +25,8 @@ import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/settings/domain/models/language_local.dart'; import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, ListPickerItem, ThunderDivider, showSnackbar, showThunderDialog; +import 'package:thunder/packages/ui/ui.dart'; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; class GeneralSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -374,729 +374,694 @@ class _GeneralSettingsPageState extends State with SingleTi ], ), ), - ListOption( - description: l10n.defaultFeedType, - value: ListPickerItem(label: defaultFeedListType.value, icon: Icons.feed, payload: defaultFeedListType), - options: [ - ListPickerItem(icon: Icons.home_rounded, label: FeedListType.all.value, payload: FeedListType.all), - ListPickerItem(icon: Icons.grid_view_rounded, label: FeedListType.local.value, payload: FeedListType.local), - ], - icon: Icons.filter_alt_rounded, - onChanged: (value) => setPreferences(LocalSettings.defaultFeedListType, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.defaultFeedListType, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.defaultFeedSortType, - value: ListPickerItem( - label: allPostSortTypeItems.firstWhere((item) => item.payload == defaultPostSortType).label, - icon: Icons.local_fire_department_rounded, - payload: defaultPostSortType, - ), - options: [...getDefaultPostSortTypeItems(), ...getTopPostSortTypeItems()], - icon: Icons.sort_rounded, - onChanged: (_) async {}, - isBottomModalScrollControlled: true, - customListPicker: SortPicker( - title: l10n.defaultFeedSortType, - onSelect: (value) async { - setPreferences(LocalSettings.defaultFeedPostSortType, value.payload.name); - }, - previouslySelected: defaultPostSortType, - ), - valueDisplay: Row( - children: [ - Icon(allPostSortTypeItems.firstWhere((item) => item.payload == defaultPostSortType).icon, size: 13), - const SizedBox(width: 4), - Text( - allPostSortTypeItems.firstWhere((item) => item.payload == defaultPostSortType).label, - style: theme.textTheme.titleSmall, - ), + ThunderListOption( + title: l10n.defaultFeedType, + value: ListPickerItem(label: defaultFeedListType.value, icon: Icons.feed, payload: defaultFeedListType), + options: [ + ListPickerItem(icon: Icons.home_rounded, label: FeedListType.all.value, payload: FeedListType.all), + ListPickerItem(icon: Icons.grid_view_rounded, label: FeedListType.local.value, payload: FeedListType.local), ], - ), - highlightKey: settingToHighlightKey, - setting: LocalSettings.defaultFeedPostSortType, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.hideNsfwPostsFromFeed, - value: hideNsfwPosts, - iconEnabled: Icons.no_adult_content, - iconDisabled: Icons.no_adult_content, - onToggle: (bool value) => setPreferences(LocalSettings.hideNsfwPosts, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.hideNsfwPosts, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.manage_accounts_rounded, - description: l10n.lookingForAccountSpecificFeedSettings, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccount), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + leading: Icon(Icons.filter_alt_rounded), + onChanged: (value) => setPreferences(LocalSettings.defaultFeedListType, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.defaultFeedListType), + highlighted: settingToHighlight == LocalSettings.defaultFeedListType), + ThunderListOption( + title: l10n.defaultFeedSortType, + value: ListPickerItem( + label: allPostSortTypeItems.firstWhere((item) => item.payload == defaultPostSortType).label, + icon: Icons.local_fire_department_rounded, + payload: defaultPostSortType, + ), + options: [...getDefaultPostSortTypeItems(), ...getTopPostSortTypeItems()], + leading: Icon(Icons.sort_rounded), + onChanged: (_) async {}, + isBottomModalScrollControlled: true, + customListPicker: SortPicker( + title: l10n.defaultFeedSortType, + onSelect: (value) async { + setPreferences(LocalSettings.defaultFeedPostSortType, value.payload.name); + }, + previouslySelected: defaultPostSortType, + ), + valueDisplay: Row( + children: [ + Icon(allPostSortTypeItems.firstWhere((item) => item.payload == defaultPostSortType).icon, size: 13), + const SizedBox(width: 4), + Text( + allPostSortTypeItems.firstWhere((item) => item.payload == defaultPostSortType).label, + style: theme.textTheme.titleSmall, + ), + ], + ), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.defaultFeedPostSortType), + highlighted: settingToHighlight == LocalSettings.defaultFeedPostSortType), + ThunderToggleOption( + title: l10n.hideNsfwPostsFromFeed, + value: hideNsfwPosts, + iconEnabled: Icons.no_adult_content, + iconDisabled: Icons.no_adult_content, + onChanged: (bool value) => setPreferences(LocalSettings.hideNsfwPosts, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.hideNsfwPosts), + highlighted: settingToHighlight == LocalSettings.hideNsfwPosts), + ThunderSettingsTile( + leading: Icon(Icons.manage_accounts_rounded), + title: l10n.lookingForAccountSpecificFeedSettings, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccount), + highlightKey: settingToHighlightKey, + highlighted: false), const ThunderDivider(sliver: false), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.feedBehaviourSettings, style: theme.textTheme.titleMedium), ), - ListOption( - description: l10n.appLanguage, - bottomSheetHeading: Align(alignment: Alignment.centerLeft, child: Text(l10n.translationsMayNotBeComplete)), - value: ListPickerItem(label: LanguageLocal.getDisplayLanguage(currentLocale.languageCode, currentLocale.toLanguageTag()), icon: Icons.language_rounded, payload: currentLocale), - options: supportedLocales.map((e) => ListPickerItem(label: LanguageLocal.getDisplayLanguage(e.languageCode, e.toLanguageTag()), icon: Icons.language_rounded, payload: e)).toList(), - icon: Icons.language_rounded, - onChanged: (ListPickerItem value) async { - setPreferences(LocalSettings.appLanguageCode, value.payload); - }, - valueDisplay: Row( - children: [ - Text( - LanguageLocal.getDisplayLanguage(currentLocale.languageCode, currentLocale.toLanguageTag()), - style: theme.textTheme.titleSmall, - ), - ], - ), - highlightKey: settingToHighlightKey, - setting: LocalSettings.appLanguageCode, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.useProfilePictureForDrawer, - subtitle: l10n.useProfilePictureForDrawerSubtitle, - value: useProfilePictureForDrawer, - iconEnabled: Icons.person_rounded, - iconDisabled: Icons.person_outline_rounded, - onToggle: (value) => setPreferences(LocalSettings.useProfilePictureForDrawer, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.useProfilePictureForDrawer, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.tappableAuthorCommunity, - value: tappableAuthorCommunity, - iconEnabled: Icons.touch_app_rounded, - iconDisabled: Icons.touch_app_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.tappableAuthorCommunity, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.tappableAuthorCommunity, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.markPostAsReadOnMediaView, - value: markPostReadOnMediaView, - iconEnabled: Icons.visibility, - iconDisabled: Icons.remove_red_eye_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.markPostAsReadOnMediaView, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.markPostAsReadOnMediaView, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.markPostAsReadOnScroll, - value: markPostReadOnScroll, - iconEnabled: Icons.playlist_add_check, - iconDisabled: Icons.playlist_add, - onToggle: (bool value) => setPreferences(LocalSettings.markPostAsReadOnScroll, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.markPostAsReadOnScroll, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.tabletMode, - value: tabletMode, - iconEnabled: Icons.tablet_rounded, - iconDisabled: Icons.smartphone_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.useTabletMode, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.useTabletMode, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.hideTopBarOnScroll, - value: hideTopBarOnScroll, - iconEnabled: Icons.vertical_align_top_rounded, - iconDisabled: Icons.vertical_align_top_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.hideTopBarOnScroll, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.hideTopBarOnScroll, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.hideBottomBarOnScroll, - value: hideBottomBarOnScroll, - iconEnabled: Icons.vertical_align_bottom_rounded, - iconDisabled: Icons.vertical_align_bottom_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.hideBottomBarOnScroll, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.hideBottomBarOnScroll, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showHiddenPosts, - value: showHiddenPosts, - iconEnabled: Icons.visibility_rounded, - iconDisabled: Icons.visibility_off_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.showHiddenPosts, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showHiddenPosts, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showExpandedTaglines, - value: showExpandedTaglines, - iconEnabled: Icons.note_rounded, - iconDisabled: Icons.note_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.showExpandedTaglines, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showExpandedTaglines, - highlightedSetting: settingToHighlight, - ), + ThunderListOption( + title: l10n.appLanguage, + bottomSheetHeading: Align(alignment: Alignment.centerLeft, child: Text(l10n.translationsMayNotBeComplete)), + value: ListPickerItem(label: LanguageLocal.getDisplayLanguage(currentLocale.languageCode, currentLocale.toLanguageTag()), icon: Icons.language_rounded, payload: currentLocale), + options: supportedLocales.map((e) => ListPickerItem(label: LanguageLocal.getDisplayLanguage(e.languageCode, e.toLanguageTag()), icon: Icons.language_rounded, payload: e)).toList(), + leading: Icon(Icons.language_rounded), + onChanged: (ListPickerItem value) async { + setPreferences(LocalSettings.appLanguageCode, value.payload); + }, + valueDisplay: Row( + children: [ + Text( + LanguageLocal.getDisplayLanguage(currentLocale.languageCode, currentLocale.toLanguageTag()), + style: theme.textTheme.titleSmall, + ), + ], + ), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.appLanguageCode), + highlighted: settingToHighlight == LocalSettings.appLanguageCode), + ThunderToggleOption( + title: l10n.useProfilePictureForDrawer, + subtitle: l10n.useProfilePictureForDrawerSubtitle, + value: useProfilePictureForDrawer, + iconEnabled: Icons.person_rounded, + iconDisabled: Icons.person_outline_rounded, + onChanged: (value) => setPreferences(LocalSettings.useProfilePictureForDrawer, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.useProfilePictureForDrawer), + highlighted: settingToHighlight == LocalSettings.useProfilePictureForDrawer), + ThunderToggleOption( + title: l10n.tappableAuthorCommunity, + value: tappableAuthorCommunity, + iconEnabled: Icons.touch_app_rounded, + iconDisabled: Icons.touch_app_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.tappableAuthorCommunity, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.tappableAuthorCommunity), + highlighted: settingToHighlight == LocalSettings.tappableAuthorCommunity), + ThunderToggleOption( + title: l10n.markPostAsReadOnMediaView, + value: markPostReadOnMediaView, + iconEnabled: Icons.visibility, + iconDisabled: Icons.remove_red_eye_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.markPostAsReadOnMediaView, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.markPostAsReadOnMediaView), + highlighted: settingToHighlight == LocalSettings.markPostAsReadOnMediaView), + ThunderToggleOption( + title: l10n.markPostAsReadOnScroll, + value: markPostReadOnScroll, + iconEnabled: Icons.playlist_add_check, + iconDisabled: Icons.playlist_add, + onChanged: (bool value) => setPreferences(LocalSettings.markPostAsReadOnScroll, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.markPostAsReadOnScroll), + highlighted: settingToHighlight == LocalSettings.markPostAsReadOnScroll), + ThunderToggleOption( + title: l10n.tabletMode, + value: tabletMode, + iconEnabled: Icons.tablet_rounded, + iconDisabled: Icons.smartphone_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.useTabletMode, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.useTabletMode), + highlighted: settingToHighlight == LocalSettings.useTabletMode), + ThunderToggleOption( + title: l10n.hideTopBarOnScroll, + value: hideTopBarOnScroll, + iconEnabled: Icons.vertical_align_top_rounded, + iconDisabled: Icons.vertical_align_top_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.hideTopBarOnScroll, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.hideTopBarOnScroll), + highlighted: settingToHighlight == LocalSettings.hideTopBarOnScroll), + ThunderToggleOption( + title: l10n.hideBottomBarOnScroll, + value: hideBottomBarOnScroll, + iconEnabled: Icons.vertical_align_bottom_rounded, + iconDisabled: Icons.vertical_align_bottom_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.hideBottomBarOnScroll, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.hideBottomBarOnScroll), + highlighted: settingToHighlight == LocalSettings.hideBottomBarOnScroll), + ThunderToggleOption( + title: l10n.showHiddenPosts, + value: showHiddenPosts, + iconEnabled: Icons.visibility_rounded, + iconDisabled: Icons.visibility_off_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.showHiddenPosts, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showHiddenPosts), + highlighted: settingToHighlight == LocalSettings.showHiddenPosts), + ThunderToggleOption( + title: l10n.showExpandedTaglines, + value: showExpandedTaglines, + iconEnabled: Icons.note_rounded, + iconDisabled: Icons.note_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.showExpandedTaglines, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showExpandedTaglines), + highlighted: settingToHighlight == LocalSettings.showExpandedTaglines), SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.commentBehaviourSettings, style: theme.textTheme.titleMedium), ), - ListOption( - description: l10n.defaultCommentSortType, - value: ListPickerItem(label: defaultCommentSortType.name, icon: Icons.local_fire_department_rounded, payload: defaultCommentSortType), - options: getCommentSortTypeItems(), - icon: Icons.comment_bank_rounded, - onChanged: (_) async {}, - customListPicker: SortPicker( - title: l10n.commentSortType, - onSelect: (value) async { - setPreferences(LocalSettings.defaultCommentSortType, value.payload.name); - }, - previouslySelected: defaultCommentSortType, - ), - valueDisplay: Row( - children: [ - Icon(getCommentSortTypeItems().firstWhere((item) => item.payload == defaultCommentSortType).icon, size: 13), - const SizedBox(width: 4), - Text( - getCommentSortTypeItems().firstWhere((item) => item.payload == defaultCommentSortType).label, - style: theme.textTheme.titleSmall, - ), - ], - ), - highlightKey: settingToHighlightKey, - setting: LocalSettings.defaultCommentSortType, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.collapseParentCommentBodyOnGesture, - value: collapseParentCommentOnGesture, - iconEnabled: Icons.mode_comment_outlined, - iconDisabled: Icons.comment_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.collapseParentCommentBodyOnGesture, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.collapseParentCommentBodyOnGesture, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.enableCommentNavigation, - value: enableCommentNavigation, - iconEnabled: Icons.unfold_more_rounded, - iconDisabled: Icons.unfold_less_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.enableCommentNavigation, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableCommentNavigation, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.combineNavAndFab, - subtitle: l10n.combineNavAndFabDescription, - value: combineNavAndFab, - iconEnabled: Icons.join_full_rounded, - iconDisabled: Icons.join_inner_rounded, - onToggle: enableCommentNavigation != true ? null : (bool value) => setPreferences(LocalSettings.combineNavAndFab, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.combineNavAndFab, - highlightedSetting: settingToHighlight, - ), + ThunderListOption( + title: l10n.defaultCommentSortType, + value: ListPickerItem(label: defaultCommentSortType.name, icon: Icons.local_fire_department_rounded, payload: defaultCommentSortType), + options: getCommentSortTypeItems(), + leading: Icon(Icons.comment_bank_rounded), + onChanged: (_) async {}, + customListPicker: SortPicker( + title: l10n.commentSortType, + onSelect: (value) async { + setPreferences(LocalSettings.defaultCommentSortType, value.payload.name); + }, + previouslySelected: defaultCommentSortType, + ), + valueDisplay: Row( + children: [ + Icon(getCommentSortTypeItems().firstWhere((item) => item.payload == defaultCommentSortType).icon, size: 13), + const SizedBox(width: 4), + Text( + getCommentSortTypeItems().firstWhere((item) => item.payload == defaultCommentSortType).label, + style: theme.textTheme.titleSmall, + ), + ], + ), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.defaultCommentSortType), + highlighted: settingToHighlight == LocalSettings.defaultCommentSortType), + ThunderToggleOption( + title: l10n.collapseParentCommentBodyOnGesture, + value: collapseParentCommentOnGesture, + iconEnabled: Icons.mode_comment_outlined, + iconDisabled: Icons.comment_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.collapseParentCommentBodyOnGesture, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.collapseParentCommentBodyOnGesture), + highlighted: settingToHighlight == LocalSettings.collapseParentCommentBodyOnGesture), + ThunderToggleOption( + title: l10n.enableCommentNavigation, + value: enableCommentNavigation, + iconEnabled: Icons.unfold_more_rounded, + iconDisabled: Icons.unfold_less_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.enableCommentNavigation, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enableCommentNavigation), + highlighted: settingToHighlight == LocalSettings.enableCommentNavigation), + ThunderToggleOption( + title: l10n.combineNavAndFab, + subtitle: l10n.combineNavAndFabDescription, + value: combineNavAndFab, + iconEnabled: Icons.join_full_rounded, + iconDisabled: Icons.join_inner_rounded, + onChanged: enableCommentNavigation != true ? null : (bool value) => setPreferences(LocalSettings.combineNavAndFab, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.combineNavAndFab), + highlighted: settingToHighlight == LocalSettings.combineNavAndFab), SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.linksBehaviourSettings, style: theme.textTheme.titleMedium), ), - ListOption( - description: l10n.browserMode, - value: ListPickerItem( - label: switch (browserMode) { - BrowserMode.inApp => l10n.linkHandlingInAppShort, - BrowserMode.customTabs => l10n.linkHandlingCustomTabsShort, - BrowserMode.external => l10n.linkHandlingExternalShort, - }, - payload: browserMode, - capitalizeLabel: false, - ), - options: [ - ListPickerItem(label: l10n.linkHandlingInApp, icon: Icons.dataset_linked_rounded, payload: BrowserMode.inApp), - ListPickerItem(label: l10n.linkHandlingCustomTabs, icon: Icons.language_rounded, payload: BrowserMode.customTabs), - ListPickerItem(label: l10n.linkHandlingExternal, icon: Icons.open_in_browser_rounded, payload: BrowserMode.external), - ], - icon: Icons.link_rounded, - onChanged: (value) => setPreferences(LocalSettings.browserMode, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.browserMode, - highlightedSetting: settingToHighlight, - ), - if (!kIsWeb && Platform.isIOS && browserMode == BrowserMode.customTabs) - ToggleOption( - description: l10n.openLinksInReaderMode, - value: openInReaderMode, - iconEnabled: Icons.menu_book_rounded, - iconDisabled: Icons.menu_book_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.openLinksInReaderMode, value), + ThunderListOption( + title: l10n.browserMode, + value: ListPickerItem( + label: switch (browserMode) { + BrowserMode.inApp => l10n.linkHandlingInAppShort, + BrowserMode.customTabs => l10n.linkHandlingCustomTabsShort, + BrowserMode.external => l10n.linkHandlingExternalShort, + }, + payload: browserMode, + capitalizeLabel: false, + ), + options: [ + ListPickerItem(label: l10n.linkHandlingInApp, icon: Icons.dataset_linked_rounded, payload: BrowserMode.inApp), + ListPickerItem(label: l10n.linkHandlingCustomTabs, icon: Icons.language_rounded, payload: BrowserMode.customTabs), + ListPickerItem(label: l10n.linkHandlingExternal, icon: Icons.open_in_browser_rounded, payload: BrowserMode.external), + ], + leading: Icon(Icons.link_rounded), + onChanged: (value) => setPreferences(LocalSettings.browserMode, value.payload.name), highlightKey: settingToHighlightKey, - setting: LocalSettings.openLinksInReaderMode, - highlightedSetting: settingToHighlight, - ), + onLongPress: () => shareLocalSetting(context, LocalSettings.browserMode), + highlighted: settingToHighlight == LocalSettings.browserMode), + if (!kIsWeb && Platform.isIOS && browserMode == BrowserMode.customTabs) + ThunderToggleOption( + title: l10n.openLinksInReaderMode, + value: openInReaderMode, + iconEnabled: Icons.menu_book_rounded, + iconDisabled: Icons.menu_book_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.openLinksInReaderMode, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.openLinksInReaderMode), + highlighted: settingToHighlight == LocalSettings.openLinksInReaderMode), // TODO:(open_lemmy_links_walkthrough) maybe have the open lemmy links walkthrough here if (!kIsWeb && Platform.isAndroid) - SettingsListTile( - icon: Icons.add_link, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - try { - const AndroidIntent intent = AndroidIntent( - action: "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS", - package: "com.hjiangsu.thunder", - data: "package:com.hjiangsu.thunder", - flags: [ANDROID_INTENT_FLAG_ACTIVITY_NEW_TASK], - ); - await intent.launch(); - } catch (e) { - openAppSettings(); - } - }, - subtitle: l10n.allowOpenSupportedLinks, - description: l10n.openByDefault, - highlightKey: settingToHighlightKey, - setting: LocalSettings.openByDefault, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.add_link), + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + try { + const AndroidIntent intent = AndroidIntent( + action: "android.settings.APP_OPEN_BY_DEFAULT_SETTINGS", + package: "com.hjiangsu.thunder", + data: "package:com.hjiangsu.thunder", + flags: [ANDROID_INTENT_FLAG_ACTIVITY_NEW_TASK], + ); + await intent.launch(); + } catch (e) { + openAppSettings(); + } + }, + subtitle: l10n.allowOpenSupportedLinks, + title: l10n.openByDefault, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.openByDefault), + highlighted: settingToHighlight == LocalSettings.openByDefault), SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.advanced, style: theme.textTheme.titleMedium), ), if (!kIsWeb && Platform.isAndroid) - ListOption( - description: l10n.imageCachingMode, - value: ListPickerItem( - label: switch (imageCachingMode) { - ImageCachingMode.aggressive => l10n.imageCachingModeAggressiveShort, - ImageCachingMode.relaxed => l10n.imageCachingModeRelaxedShort, - }, - payload: imageCachingMode, - capitalizeLabel: false, - ), - options: [ - ListPickerItem(icon: Icons.broken_image, label: l10n.imageCachingModeAggressive, payload: ImageCachingMode.aggressive, capitalizeLabel: false), - ListPickerItem(icon: Icons.broken_image_outlined, label: l10n.imageCachingModeRelaxed, payload: ImageCachingMode.relaxed, capitalizeLabel: false), - ], - icon: switch (imageCachingMode) { - ImageCachingMode.aggressive => Icons.broken_image, - ImageCachingMode.relaxed => Icons.broken_image_outlined, - }, - onChanged: (value) => setPreferences(LocalSettings.imageCachingMode, value.payload.name), + ThunderListOption( + title: l10n.imageCachingMode, + value: ListPickerItem( + label: switch (imageCachingMode) { + ImageCachingMode.aggressive => l10n.imageCachingModeAggressiveShort, + ImageCachingMode.relaxed => l10n.imageCachingModeRelaxedShort, + }, + payload: imageCachingMode, + capitalizeLabel: false, + ), + options: [ + ListPickerItem(icon: Icons.broken_image, label: l10n.imageCachingModeAggressive, payload: ImageCachingMode.aggressive, capitalizeLabel: false), + ListPickerItem(icon: Icons.broken_image_outlined, label: l10n.imageCachingModeRelaxed, payload: ImageCachingMode.relaxed, capitalizeLabel: false), + ], + leading: Icon(switch (imageCachingMode) { + ImageCachingMode.aggressive => Icons.broken_image, + ImageCachingMode.relaxed => Icons.broken_image_outlined, + }), + onChanged: (value) => setPreferences(LocalSettings.imageCachingMode, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.imageCachingMode), + highlighted: settingToHighlight == LocalSettings.imageCachingMode), + ThunderToggleOption( + title: l10n.showNavigationLabels, + subtitle: l10n.showNavigationLabelsDescription, + value: showNavigationLabels, + iconEnabled: Icons.short_text_rounded, + iconDisabled: Icons.short_text_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.showNavigationLabels, value), highlightKey: settingToHighlightKey, - setting: LocalSettings.imageCachingMode, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showNavigationLabels, - subtitle: l10n.showNavigationLabelsDescription, - value: showNavigationLabels, - iconEnabled: Icons.short_text_rounded, - iconDisabled: Icons.short_text_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.showNavigationLabels, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showNavigationLabels, - highlightedSetting: settingToHighlight, - ), + onLongPress: () => shareLocalSetting(context, LocalSettings.showNavigationLabels), + highlighted: settingToHighlight == LocalSettings.showNavigationLabels), SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.notificationsBehaviourSettings, style: theme.textTheme.titleMedium), ), - ToggleOption( - description: l10n.showInAppUpdateNotifications, - value: showInAppUpdateNotification, - iconEnabled: Icons.update_rounded, - iconDisabled: Icons.update_disabled_rounded, - onToggle: (bool value) => setPreferences(LocalSettings.showInAppUpdateNotification, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showInAppUpdateNotification, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showUpdateChangelogs, - subtitle: l10n.showUpdateChangelogsSubtitle, - value: showUpdateChangelogs, - iconEnabled: Icons.featured_play_list_rounded, - iconDisabled: Icons.featured_play_list_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.showUpdateChangelogs, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.showUpdateChangelogs, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.showInAppUpdateNotifications, + value: showInAppUpdateNotification, + iconEnabled: Icons.update_rounded, + iconDisabled: Icons.update_disabled_rounded, + onChanged: (bool value) => setPreferences(LocalSettings.showInAppUpdateNotification, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showInAppUpdateNotification), + highlighted: settingToHighlight == LocalSettings.showInAppUpdateNotification), + ThunderToggleOption( + title: l10n.showUpdateChangelogs, + subtitle: l10n.showUpdateChangelogsSubtitle, + value: showUpdateChangelogs, + iconEnabled: Icons.featured_play_list_rounded, + iconDisabled: Icons.featured_play_list_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.showUpdateChangelogs, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.showUpdateChangelogs), + highlighted: settingToHighlight == LocalSettings.showUpdateChangelogs), if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) ...[ - ListOption( - description: l10n.enableInboxNotifications, - subtitleWidget: Text.rich( - style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.8)), - softWrap: true, - TextSpan( - children: [ - TextSpan(text: accounts.isEmpty ? l10n.loginToPerformAction : inboxNotificationType.toString()), - if (Platform.isAndroid && - (inboxNotificationType == NotificationType.local || inboxNotificationType == NotificationType.unifiedPush) && - areAndroidNotificationsAllowed != true) ...[ - const TextSpan(text: '\n'), - TextSpan( - text: '- ${l10n.notificationsNotAllowed}', - style: theme.textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: Colors.red.withValues(alpha: 0.8), + ThunderListOption( + title: l10n.enableInboxNotifications, + subtitleWidget: Text.rich( + style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.8)), + softWrap: true, + TextSpan( + children: [ + TextSpan(text: accounts.isEmpty ? l10n.loginToPerformAction : inboxNotificationType.toString()), + if (Platform.isAndroid && + (inboxNotificationType == NotificationType.local || inboxNotificationType == NotificationType.unifiedPush) && + areAndroidNotificationsAllowed != true) ...[ + const TextSpan(text: '\n'), + TextSpan( + text: '- ${l10n.notificationsNotAllowed}', + style: theme.textTheme.bodySmall?.copyWith( + fontStyle: FontStyle.italic, + color: Colors.red.withValues(alpha: 0.8), + ), ), - ), - ], - if (Platform.isAndroid && inboxNotificationType == NotificationType.unifiedPush) ...[ - if (unifiedPushConnectedDistributorApp?.isNotEmpty != true) ...[ - if ((unifiedPushAvailableDistributorApps ?? 0) == 1) ...[ - const TextSpan(text: '\n'), - TextSpan( - text: '- ${l10n.foundUnifiedPushDistribtorApp}', - style: theme.textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: Colors.red.withValues(alpha: 0.8), + ], + if (Platform.isAndroid && inboxNotificationType == NotificationType.unifiedPush) ...[ + if (unifiedPushConnectedDistributorApp?.isNotEmpty != true) ...[ + if ((unifiedPushAvailableDistributorApps ?? 0) == 1) ...[ + const TextSpan(text: '\n'), + TextSpan( + text: '- ${l10n.foundUnifiedPushDistribtorApp}', + style: theme.textTheme.bodySmall?.copyWith( + fontStyle: FontStyle.italic, + color: Colors.red.withValues(alpha: 0.8), + ), ), - ), - ], - if ((unifiedPushAvailableDistributorApps ?? 0) > 1) ...[ - const TextSpan(text: '\n'), - TextSpan( - text: '- ${l10n.doNotSupportMultipleUnifiedPushApps}', - style: theme.textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: Colors.red.withValues(alpha: 0.8), + ], + if ((unifiedPushAvailableDistributorApps ?? 0) > 1) ...[ + const TextSpan(text: '\n'), + TextSpan( + text: '- ${l10n.doNotSupportMultipleUnifiedPushApps}', + style: theme.textTheme.bodySmall?.copyWith( + fontStyle: FontStyle.italic, + color: Colors.red.withValues(alpha: 0.8), + ), ), - ), + ], + if ((unifiedPushAvailableDistributorApps ?? 0) == 0) ...[ + const TextSpan(text: '\n'), + TextSpan( + text: '- ${l10n.noCompatibleAppFound}', + style: theme.textTheme.bodySmall?.copyWith( + fontStyle: FontStyle.italic, + color: Colors.red.withValues(alpha: 0.8), + ), + ), + ], ], - if ((unifiedPushAvailableDistributorApps ?? 0) == 0) ...[ + if (unifiedPushConnectedDistributorApp?.isNotEmpty == true) ...[ const TextSpan(text: '\n'), TextSpan( - text: '- ${l10n.noCompatibleAppFound}', + text: l10n.connectedToUnifiedPushDistributorApp(unifiedPushConnectedDistributorApp!), style: theme.textTheme.bodySmall?.copyWith( fontStyle: FontStyle.italic, - color: Colors.red.withValues(alpha: 0.8), + color: Colors.green.withValues(alpha: 0.8), ), ), ], ], - if (unifiedPushConnectedDistributorApp?.isNotEmpty == true) ...[ - const TextSpan(text: '\n'), - TextSpan( - text: l10n.connectedToUnifiedPushDistributorApp(unifiedPushConnectedDistributorApp!), - style: theme.textTheme.bodySmall?.copyWith( - fontStyle: FontStyle.italic, - color: Colors.green.withValues(alpha: 0.8), - ), - ), - ], ], - ], + ), ), - ), - value: const ListPickerItem(payload: -1), - disabled: accounts.isEmpty, - icon: inboxNotificationType == NotificationType.none ? Icons.notifications_off_rounded : Icons.notifications_on_rounded, - highlightKey: settingToHighlightKey, - setting: LocalSettings.inboxNotificationType, - highlightedSetting: settingToHighlight, - customListPicker: StatefulBuilder( - builder: (context, setState) { - return BottomSheetListPicker( - title: l10n.pushNotification, - heading: Align( - alignment: Alignment.centerLeft, - child: CommonMarkdownBody(body: l10n.pushNotificationDescription), - ), - previouslySelected: inboxNotificationType, - items: Platform.isAndroid - ? [ - ListPickerItem( - icon: Icons.notifications_off_rounded, - label: l10n.none, - payload: NotificationType.none, - softWrap: true, - ), - ListPickerItem( - icon: Icons.notifications_rounded, - label: l10n.useLocalNotifications, - subtitle: l10n.useLocalNotificationsDescription, - payload: NotificationType.local, - softWrap: true, - ), - if (enableExperimentalFeatures) + value: const ListPickerItem(payload: -1), + options: const [], + disabled: accounts.isEmpty, + leading: Icon(inboxNotificationType == NotificationType.none ? Icons.notifications_off_rounded : Icons.notifications_on_rounded), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.inboxNotificationType), + highlighted: settingToHighlight == LocalSettings.inboxNotificationType, + customListPicker: StatefulBuilder( + builder: (context, setState) { + return BottomSheetListPicker( + title: l10n.pushNotification, + heading: Align( + alignment: Alignment.centerLeft, + child: CommonMarkdownBody(body: l10n.pushNotificationDescription), + ), + previouslySelected: inboxNotificationType, + items: Platform.isAndroid + ? [ ListPickerItem( - icon: Icons.notifications_active_rounded, - label: l10n.useUnifiedPushNotifications, - subtitleWidget: Text.rich( - style: theme.textTheme.bodyMedium?.copyWith(color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5)), - softWrap: true, - TextSpan( - children: [ - TextSpan(text: l10n.useUnifiedPushNotificationsDescription), - const TextSpan(text: ' ('), - TextSpan(text: l10n.suchAs), - const TextSpan(text: ' '), - TextSpan( - text: 'ntfy', - style: theme.textTheme.bodyMedium?.copyWith(color: Colors.blue), - recognizer: TapGestureRecognizer() - ..onTap = () { - handleLink(context, url: 'https://f-droid.org/packages/io.heckel.ntfy/'); - }, - ), - const TextSpan(text: ')'), - ], - ), - ), - payload: NotificationType.unifiedPush, + icon: Icons.notifications_off_rounded, + label: l10n.none, + payload: NotificationType.none, softWrap: true, ), - ] - : [ - ListPickerItem( - icon: Icons.notifications_off_rounded, - label: l10n.disablePushNotifications, - payload: NotificationType.none, - softWrap: true, - ), - if (enableExperimentalFeatures) ListPickerItem( - icon: Icons.notifications_active_rounded, - label: l10n.useApplePushNotifications, - subtitle: l10n.useApplePushNotificationsDescription, - payload: NotificationType.apn, + icon: Icons.notifications_rounded, + label: l10n.useLocalNotifications, + subtitle: l10n.useLocalNotificationsDescription, + payload: NotificationType.local, softWrap: true, ), - ], - onSelect: (ListPickerItem notificationType) async { - if (notificationType.payload == inboxNotificationType) { - return; - } - - bool success = await updateNotificationSettings( - context, - currentNotificationType: inboxNotificationType, - updatedNotificationType: notificationType.payload, - onUpdate: (NotificationType updatedNotificationType) async { - setPreferences(LocalSettings.inboxNotificationType, updatedNotificationType); - - if (Platform.isAndroid) { - areAndroidNotificationsAllowed = await androidFlutterLocalNotificationsPlugin?.areNotificationsEnabled(); - - if (updatedNotificationType == NotificationType.unifiedPush) { - unifiedPushConnectedDistributorApp = await UnifiedPush.getDistributor(); - unifiedPushAvailableDistributorApps = (await UnifiedPush.getDistributors()).length; + if (enableExperimentalFeatures) + ListPickerItem( + icon: Icons.notifications_active_rounded, + label: l10n.useUnifiedPushNotifications, + subtitleWidget: Text.rich( + style: theme.textTheme.bodyMedium?.copyWith(color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5)), + softWrap: true, + TextSpan( + children: [ + TextSpan(text: l10n.useUnifiedPushNotificationsDescription), + const TextSpan(text: ' ('), + TextSpan(text: l10n.suchAs), + const TextSpan(text: ' '), + TextSpan( + text: 'ntfy', + style: theme.textTheme.bodyMedium?.copyWith(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () { + handleLink(context, url: 'https://f-droid.org/packages/io.heckel.ntfy/'); + }, + ), + const TextSpan(text: ')'), + ], + ), + ), + payload: NotificationType.unifiedPush, + softWrap: true, + ), + ] + : [ + ListPickerItem( + icon: Icons.notifications_off_rounded, + label: l10n.disablePushNotifications, + payload: NotificationType.none, + softWrap: true, + ), + if (enableExperimentalFeatures) + ListPickerItem( + icon: Icons.notifications_active_rounded, + label: l10n.useApplePushNotifications, + subtitle: l10n.useApplePushNotificationsDescription, + payload: NotificationType.apn, + softWrap: true, + ), + ], + onSelect: (ListPickerItem notificationType) async { + if (notificationType.payload == inboxNotificationType) { + return; + } + + bool success = await updateNotificationSettings( + context, + currentNotificationType: inboxNotificationType, + updatedNotificationType: notificationType.payload, + onUpdate: (NotificationType updatedNotificationType) async { + setPreferences(LocalSettings.inboxNotificationType, updatedNotificationType); + + if (Platform.isAndroid) { + areAndroidNotificationsAllowed = await androidFlutterLocalNotificationsPlugin?.areNotificationsEnabled(); + + if (updatedNotificationType == NotificationType.unifiedPush) { + unifiedPushConnectedDistributorApp = await UnifiedPush.getDistributor(); + unifiedPushAvailableDistributorApps = (await UnifiedPush.getDistributors()).length; + } } - } - }, - ); - - if (!success) { - showSnackbar(l10n.failedToUpdateNotificationSettings); - } - _initPreferences(); - }, - ); - }, - ), - ), + }, + ); + + if (!success) { + showSnackbar(l10n.failedToUpdateNotificationSettings); + } + _initPreferences(); + }, + ); + }, + )), if (inboxNotificationType == NotificationType.unifiedPush || inboxNotificationType == NotificationType.apn) - SettingsListTile( - icon: Icons.electrical_services_rounded, - description: l10n.pushNotificationServer, - subtitle: pushNotificationServer, - widget: const SizedBox( + ThunderSettingsTile( + leading: Icon(Icons.electrical_services_rounded), + title: l10n.pushNotificationServer, + subtitle: pushNotificationServer, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + showThunderDialog( + context: context, + title: l10n.pushNotificationServer, + contentWidgetBuilder: (_) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CommonMarkdownBody(body: l10n.pushNotificationServerDescription), + const SizedBox(height: 32.0), + TextField( + textInputAction: TextInputAction.done, + keyboardType: TextInputType.url, + autocorrect: false, + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: l10n.url, + hintText: THUNDER_SERVER_URL, + ), + enableSuggestions: false, + ), + ], + ); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + primaryButtonText: l10n.confirm, + onPrimaryButtonPressed: (dialogContext, _) { + setPreferences(LocalSettings.pushNotificationServer, controller.text); + Navigator.of(dialogContext).pop(); + }, + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.pushNotificationServer), + highlighted: settingToHighlight == LocalSettings.pushNotificationServer), + ThunderSettingsTile( + leading: Icon(Icons.bug_report_rounded), + title: l10n.havingIssuesWithNotifications, + trailing: const SizedBox( height: 42.0, child: Icon(Icons.chevron_right_rounded), ), - onTap: () async { - showThunderDialog( - context: context, - title: l10n.pushNotificationServer, - contentWidgetBuilder: (_) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - CommonMarkdownBody(body: l10n.pushNotificationServerDescription), - const SizedBox(height: 32.0), - TextField( - textInputAction: TextInputAction.done, - keyboardType: TextInputType.url, - autocorrect: false, - controller: controller, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: l10n.url, - hintText: THUNDER_SERVER_URL, - ), - enableSuggestions: false, - ), - ], - ); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - primaryButtonText: l10n.confirm, - onPrimaryButtonPressed: (dialogContext, _) { - setPreferences(LocalSettings.pushNotificationServer, controller.text); - Navigator.of(dialogContext).pop(); - }, - ); - }, + onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageDebug), highlightKey: settingToHighlightKey, - setting: LocalSettings.pushNotificationServer, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.bug_report_rounded, - description: l10n.havingIssuesWithNotifications, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageDebug), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + highlighted: false), ], SizedBox(height: 16.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.importExportSettings, style: theme.textTheme.titleMedium), ), - SettingsListTile( - icon: Icons.settings_rounded, - description: l10n.saveSettings, - subtitle: l10n.exportSettingsSubtitle, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - String? savedFilePath = await UserPreferences.exportToJson(); - - if (savedFilePath?.isNotEmpty == true) { - showSnackbar(l10n.settingsExportedSuccessfully(savedFilePath!)); - } else { - showSnackbar(l10n.settingsNotExportedSuccessfully); - } - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.importExportSettings, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.import_export_rounded, - description: l10n.importSettings, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - bool? importedSuccessfully = await UserPreferences.importFromJson(); + ThunderSettingsTile( + leading: Icon(Icons.settings_rounded), + title: l10n.saveSettings, + subtitle: l10n.exportSettingsSubtitle, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + String? savedFilePath = await UserPreferences.exportToJson(); - if (importedSuccessfully == true) { - showSnackbar(l10n.settingsImportedSuccessfully); + if (savedFilePath?.isNotEmpty == true) { + showSnackbar(l10n.settingsExportedSuccessfully(savedFilePath!)); + } else { + showSnackbar(l10n.settingsNotExportedSuccessfully); + } + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.importExportSettings), + highlighted: settingToHighlight == LocalSettings.importExportSettings), + ThunderSettingsTile( + leading: Icon(Icons.import_export_rounded), + title: l10n.importSettings, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + bool? importedSuccessfully = await UserPreferences.importFromJson(); + + if (importedSuccessfully == true) { + showSnackbar(l10n.settingsImportedSuccessfully); + + if (context.mounted) { + _initPreferences(); + context.read().add(UserPreferencesChangeEvent()); + context.read().reload(); + } else { + showSnackbar(l10n.settingsNotImportedSuccessfully); + } + } + }, + highlightKey: settingToHighlightKey, + highlighted: false), + ThunderSettingsTile( + leading: Icon(Icons.dashboard_customize_rounded), + title: l10n.exportDatabase, + subtitle: l10n.exportDatabaseSubtitle, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + bool result = false; + + await showThunderDialog( + context: context, + title: l10n.warning, + contentText: l10n.databaseExportWarning, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: l10n.cancel, + onPrimaryButtonPressed: (dialogContext, _) async { + Navigator.of(dialogContext).pop(); + result = true; + }, + primaryButtonText: l10n.yes, + ); + + if (!result) return; + + String? savedFilePath = await exportDatabase(); + + if (savedFilePath?.isNotEmpty == true) { + showSnackbar(l10n.databaseExportedSuccessfully(savedFilePath!)); + } else { + showSnackbar(l10n.databaseNotExportedSuccessfully); + } + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.importExportDatabase), + highlighted: settingToHighlight == LocalSettings.importExportDatabase), + ThunderSettingsTile( + leading: Icon(Icons.dashboard_customize_outlined), + title: l10n.importDatabase, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + bool importedSuccessfully = await importDatabase(); - if (context.mounted) { - _initPreferences(); - context.read().add(UserPreferencesChangeEvent()); - context.read().reload(); + if (importedSuccessfully == true) { + showSnackbar(l10n.databaseImportedSuccessfully); } else { - showSnackbar(l10n.settingsNotImportedSuccessfully); + showSnackbar(l10n.databaseNotImportedSuccessfully); } - } - }, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.dashboard_customize_rounded, - description: l10n.exportDatabase, - subtitle: l10n.exportDatabaseSubtitle, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - bool result = false; - - await showThunderDialog( - context: context, - title: l10n.warning, - contentText: l10n.databaseExportWarning, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - secondaryButtonText: l10n.cancel, - onPrimaryButtonPressed: (dialogContext, _) async { - Navigator.of(dialogContext).pop(); - result = true; - }, - primaryButtonText: l10n.yes, - ); - - if (!result) return; - - String? savedFilePath = await exportDatabase(); - - if (savedFilePath?.isNotEmpty == true) { - showSnackbar(l10n.databaseExportedSuccessfully(savedFilePath!)); - } else { - showSnackbar(l10n.databaseNotExportedSuccessfully); - } - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.importExportDatabase, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.dashboard_customize_outlined, - description: l10n.importDatabase, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - bool importedSuccessfully = await importDatabase(); - - if (importedSuccessfully == true) { - showSnackbar(l10n.databaseImportedSuccessfully); - } else { - showSnackbar(l10n.databaseNotImportedSuccessfully); - } - }, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + }, + highlightKey: settingToHighlightKey, + highlighted: false), SizedBox(height: 128.0), ], ), diff --git a/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart b/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart index 1ac637b69..5ec53dc69 100644 --- a/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/behavior/gesture_settings_page.dart @@ -12,7 +12,7 @@ import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show ListPickerItem; +import 'package:thunder/packages/ui/ui.dart'; class GestureSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -247,17 +247,16 @@ class _GestureSettingsPageState extends State with TickerPr style: theme.textTheme.titleMedium, ), ), - ToggleOption( - description: l10n.fullscreenSwipeGestures, - subtitle: l10n.fullScreenNavigationSwipeDescription, - value: enableFullScreenSwipeNavigationGesture, - iconEnabled: Icons.swipe_left_rounded, - iconDisabled: Icons.swipe_left_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.enableFullScreenSwipeNavigationGesture, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableFullScreenSwipeNavigationGesture, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.fullscreenSwipeGestures, + subtitle: l10n.fullScreenNavigationSwipeDescription, + value: enableFullScreenSwipeNavigationGesture, + iconEnabled: Icons.swipe_left_rounded, + iconDisabled: Icons.swipe_left_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.enableFullScreenSwipeNavigationGesture, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enableFullScreenSwipeNavigationGesture), + highlighted: settingToHighlight == LocalSettings.enableFullScreenSwipeNavigationGesture), ], ), ), @@ -274,28 +273,26 @@ class _GestureSettingsPageState extends State with TickerPr style: theme.textTheme.titleMedium, ), ), - ToggleOption( - description: l10n.navbarSwipeGestures, - subtitle: l10n.sidebarBottomNavSwipeDescription, - value: bottomNavBarSwipeGestures, - iconEnabled: Icons.swipe_right_rounded, - iconDisabled: Icons.swipe_right_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.sidebarBottomNavBarSwipeGesture, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.sidebarBottomNavBarSwipeGesture, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.navbarDoubleTapGestures, - subtitle: l10n.sidebarBottomNavDoubleTapDescription, - value: bottomNavBarDoubleTapGestures, - iconEnabled: Icons.touch_app_rounded, - iconDisabled: Icons.touch_app_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.sidebarBottomNavBarDoubleTapGesture, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.sidebarBottomNavBarDoubleTapGesture, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.navbarSwipeGestures, + subtitle: l10n.sidebarBottomNavSwipeDescription, + value: bottomNavBarSwipeGestures, + iconEnabled: Icons.swipe_right_rounded, + iconDisabled: Icons.swipe_right_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.sidebarBottomNavBarSwipeGesture, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.sidebarBottomNavBarSwipeGesture), + highlighted: settingToHighlight == LocalSettings.sidebarBottomNavBarSwipeGesture), + ThunderToggleOption( + title: l10n.navbarDoubleTapGestures, + subtitle: l10n.sidebarBottomNavDoubleTapDescription, + value: bottomNavBarDoubleTapGestures, + iconEnabled: Icons.touch_app_rounded, + iconDisabled: Icons.touch_app_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.sidebarBottomNavBarDoubleTapGesture, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.sidebarBottomNavBarDoubleTapGesture), + highlighted: settingToHighlight == LocalSettings.sidebarBottomNavBarDoubleTapGesture), ], ), ), @@ -312,23 +309,22 @@ class _GestureSettingsPageState extends State with TickerPr style: theme.textTheme.titleMedium, ), ), - ListOption( - description: l10n.imagePeekDuration, - subtitle: l10n.imagePeekDurationDescription, - value: ListPickerItem(label: '${imagePeekDuration}ms', icon: Icons.touch_app_rounded, payload: imagePeekDuration), - options: [ - ListPickerItem(icon: Icons.touch_app_rounded, label: '100ms', payload: 100), - ListPickerItem(icon: Icons.touch_app_rounded, label: '200ms', payload: 200), - ListPickerItem(icon: Icons.touch_app_rounded, label: '300ms', payload: 300), - ListPickerItem(icon: Icons.touch_app_rounded, label: '400ms', payload: 400), - ListPickerItem(icon: Icons.touch_app_rounded, label: '500ms', payload: 500), - ], - icon: Icons.touch_app_rounded, - onChanged: (value) async => setPreferences(LocalSettings.imagePeekDuration, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.imagePeekDuration, - highlightedSetting: settingToHighlight, - ), + ThunderListOption( + title: l10n.imagePeekDuration, + subtitle: l10n.imagePeekDurationDescription, + value: ListPickerItem(label: '${imagePeekDuration}ms', icon: Icons.touch_app_rounded, payload: imagePeekDuration), + options: [ + ListPickerItem(icon: Icons.touch_app_rounded, label: '100ms', payload: 100), + ListPickerItem(icon: Icons.touch_app_rounded, label: '200ms', payload: 200), + ListPickerItem(icon: Icons.touch_app_rounded, label: '300ms', payload: 300), + ListPickerItem(icon: Icons.touch_app_rounded, label: '400ms', payload: 400), + ListPickerItem(icon: Icons.touch_app_rounded, label: '500ms', payload: 500), + ], + leading: Icon(Icons.touch_app_rounded), + onChanged: (value) async => setPreferences(LocalSettings.imagePeekDuration, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.imagePeekDuration), + highlighted: settingToHighlight == LocalSettings.imagePeekDuration), ], ), ), @@ -354,16 +350,15 @@ class _GestureSettingsPageState extends State with TickerPr ), ), ), - ToggleOption( - description: l10n.postSwipeActions, - value: enablePostGestures, - iconEnabled: Icons.swipe_rounded, - iconDisabled: Icons.swipe_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.enablePostGestures, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enablePostGestures, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.postSwipeActions, + value: enablePostGestures, + iconEnabled: Icons.swipe_rounded, + iconDisabled: Icons.swipe_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.enablePostGestures, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enablePostGestures), + highlighted: settingToHighlight == LocalSettings.enablePostGestures), AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeInOut, @@ -461,16 +456,15 @@ class _GestureSettingsPageState extends State with TickerPr ), ), ), - ToggleOption( - description: l10n.commentSwipeActions, - value: enableCommentGestures, - iconEnabled: Icons.swipe_rounded, - iconDisabled: Icons.swipe_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.enableCommentGestures, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableCommentGestures, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.commentSwipeActions, + value: enableCommentGestures, + iconEnabled: Icons.swipe_rounded, + iconDisabled: Icons.swipe_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.enableCommentGestures, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enableCommentGestures), + highlighted: settingToHighlight == LocalSettings.enableCommentGestures), AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeInOut, @@ -544,18 +538,16 @@ class _GestureSettingsPageState extends State with TickerPr : null, ), const SizedBox(height: 10), - SettingsListTile( - icon: Icons.color_lens_rounded, - description: l10n.actionColorsRedirect, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () => navigateToSettingPage(context, LocalSettings.actionColors), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.color_lens_rounded), + title: l10n.actionColorsRedirect, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () => navigateToSettingPage(context, LocalSettings.actionColors), + highlightKey: settingToHighlightKey, + highlighted: false), ], ), ), diff --git a/lib/src/features/settings/presentation/pages/behavior/video_player_settings.dart b/lib/src/features/settings/presentation/pages/behavior/video_player_settings.dart index 166685d3c..2f8f8ab2b 100644 --- a/lib/src/features/settings/presentation/pages/behavior/video_player_settings.dart +++ b/lib/src/features/settings/presentation/pages/behavior/video_player_settings.dart @@ -10,7 +10,7 @@ import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; -import 'package:thunder/packages/ui/ui.dart' show ListPickerItem; +import 'package:thunder/packages/ui/ui.dart'; class VideoPlayerSettingsPage extends StatefulWidget { const VideoPlayerSettingsPage({super.key, this.settingToHighlight}); @@ -135,98 +135,92 @@ class _VideoPlayerSettingsPageState extends State { SliverAppBar(title: Text(l10n.video), centerTitle: false, toolbarHeight: APP_BAR_HEIGHT, pinned: true), SliverList.list( children: [ - ToggleOption( - description: l10n.videoAutoFullscreen, - value: videoAutoFullscreen, - iconEnabled: Icons.fullscreen, - iconDisabled: Icons.fullscreen_exit, - onToggle: (bool value) => setPreferences(LocalSettings.videoAutoFullscreen, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.videoAutoFullscreen, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.videoAutoMute, - value: videoAutoMute, - iconEnabled: Icons.volume_off, - iconDisabled: Icons.volume_up, - onToggle: (bool value) => setPreferences(LocalSettings.videoAutoMute, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.videoAutoMute, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.videoAutoLoop, - value: videoAutoLoop, - iconEnabled: Icons.loop, - iconDisabled: Icons.loop_outlined, - onToggle: (bool value) => setPreferences(LocalSettings.videoAutoLoop, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.videoAutoLoop, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.videoAutoPlay, - value: ListPickerItem( - label: switch (videoAutoPlay) { - VideoAutoPlay.never => l10n.never, - VideoAutoPlay.always => l10n.always, - VideoAutoPlay.onWifi => l10n.onWifi, + ThunderToggleOption( + title: l10n.videoAutoFullscreen, + value: videoAutoFullscreen, + iconEnabled: Icons.fullscreen, + iconDisabled: Icons.fullscreen_exit, + onChanged: (bool value) => setPreferences(LocalSettings.videoAutoFullscreen, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.videoAutoFullscreen), + highlighted: settingToHighlight == LocalSettings.videoAutoFullscreen), + ThunderToggleOption( + title: l10n.videoAutoMute, + value: videoAutoMute, + iconEnabled: Icons.volume_off, + iconDisabled: Icons.volume_up, + onChanged: (bool value) => setPreferences(LocalSettings.videoAutoMute, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.videoAutoMute), + highlighted: settingToHighlight == LocalSettings.videoAutoMute), + ThunderToggleOption( + title: l10n.videoAutoLoop, + value: videoAutoLoop, + iconEnabled: Icons.loop, + iconDisabled: Icons.loop_outlined, + onChanged: (bool value) => setPreferences(LocalSettings.videoAutoLoop, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.videoAutoLoop), + highlighted: settingToHighlight == LocalSettings.videoAutoLoop), + ThunderListOption( + title: l10n.videoAutoPlay, + value: ListPickerItem( + label: switch (videoAutoPlay) { + VideoAutoPlay.never => l10n.never, + VideoAutoPlay.always => l10n.always, + VideoAutoPlay.onWifi => l10n.onWifi, + }, + icon: Icons.video_settings_outlined, + payload: videoAutoPlay), + options: [ + ListPickerItem(icon: Icons.not_interested, label: l10n.never, payload: VideoAutoPlay.never), + ListPickerItem(icon: Icons.play_arrow, label: l10n.always, payload: VideoAutoPlay.always), + ListPickerItem(icon: Icons.wifi, label: l10n.onWifi, payload: VideoAutoPlay.onWifi), + ], + leading: Icon(Icons.play_circle), + onChanged: (value) async => setPreferences(LocalSettings.videoAutoPlay, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.videoAutoPlay), + highlighted: settingToHighlight == LocalSettings.videoAutoPlay), + ThunderListOption( + title: l10n.videoDefaultPlaybackSpeed, + value: ListPickerItem(label: videoDefaultPlaybackSpeed.label, icon: Icons.speed, payload: videoDefaultPlaybackSpeed), + options: [ + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.pointTow5x.label, payload: VideoPlayBackSpeed.pointTow5x), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.point5x.label, payload: VideoPlayBackSpeed.point5x), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.pointSeven5x.label, payload: VideoPlayBackSpeed.pointSeven5x), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.normal.label, payload: VideoPlayBackSpeed.normal), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.onePointTwo5x.label, payload: VideoPlayBackSpeed.onePointTwo5x), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.onePoint5x.label, payload: VideoPlayBackSpeed.onePoint5x), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.onePointSeven5x.label, payload: VideoPlayBackSpeed.onePointSeven5x), + ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.twoX.label, payload: VideoPlayBackSpeed.twoX), + ], + leading: Icon(Icons.speed), + onChanged: (value) async => setPreferences(LocalSettings.videoDefaultPlaybackSpeed, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.videoDefaultPlaybackSpeed), + highlighted: settingToHighlight == LocalSettings.videoDefaultPlaybackSpeed), + ThunderListOption( + title: l10n.videoPlayerMode, + value: ListPickerItem( + label: switch (videoPlayerMode) { + VideoPlayerMode.inApp => l10n.videoPlayerInApp, + VideoPlayerMode.customTabs => l10n.linkHandlingCustomTabsShort, + VideoPlayerMode.externalPlayer => l10n.linkHandlingExternalShort, }, - icon: Icons.video_settings_outlined, - payload: videoAutoPlay), - options: [ - ListPickerItem(icon: Icons.not_interested, label: l10n.never, payload: VideoAutoPlay.never), - ListPickerItem(icon: Icons.play_arrow, label: l10n.always, payload: VideoAutoPlay.always), - ListPickerItem(icon: Icons.wifi, label: l10n.onWifi, payload: VideoAutoPlay.onWifi), - ], - icon: Icons.play_circle, - onChanged: (value) async => setPreferences(LocalSettings.videoAutoPlay, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.videoAutoPlay, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.videoDefaultPlaybackSpeed, - value: ListPickerItem(label: videoDefaultPlaybackSpeed.label, icon: Icons.speed, payload: videoDefaultPlaybackSpeed), - options: [ - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.pointTow5x.label, payload: VideoPlayBackSpeed.pointTow5x), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.point5x.label, payload: VideoPlayBackSpeed.point5x), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.pointSeven5x.label, payload: VideoPlayBackSpeed.pointSeven5x), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.normal.label, payload: VideoPlayBackSpeed.normal), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.onePointTwo5x.label, payload: VideoPlayBackSpeed.onePointTwo5x), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.onePoint5x.label, payload: VideoPlayBackSpeed.onePoint5x), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.onePointSeven5x.label, payload: VideoPlayBackSpeed.onePointSeven5x), - ListPickerItem(icon: Icons.speed, label: VideoPlayBackSpeed.twoX.label, payload: VideoPlayBackSpeed.twoX), - ], - icon: Icons.speed, - onChanged: (value) async => setPreferences(LocalSettings.videoDefaultPlaybackSpeed, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.videoDefaultPlaybackSpeed, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.videoPlayerMode, - value: ListPickerItem( - label: switch (videoPlayerMode) { - VideoPlayerMode.inApp => l10n.videoPlayerInApp, - VideoPlayerMode.customTabs => l10n.linkHandlingCustomTabsShort, - VideoPlayerMode.externalPlayer => l10n.linkHandlingExternalShort, - }, - payload: videoPlayerMode, - capitalizeLabel: false, - ), - options: [ - ListPickerItem(label: l10n.videoPlayerInApp, icon: Icons.play_circle_fill, payload: VideoPlayerMode.inApp), - ListPickerItem(label: l10n.linkHandlingCustomTabs, icon: Icons.language_rounded, payload: VideoPlayerMode.customTabs), - ListPickerItem(label: l10n.videoLinkHandlingExternal, icon: Icons.open_in_browser_rounded, payload: VideoPlayerMode.externalPlayer), - ], - icon: Icons.video_label_outlined, - onChanged: (value) => setPreferences(LocalSettings.videoPlayerMode, value.payload.name), - highlightKey: settingToHighlightKey, - setting: LocalSettings.videoPlayerMode, - highlightedSetting: settingToHighlight, - ), + payload: videoPlayerMode, + capitalizeLabel: false, + ), + options: [ + ListPickerItem(label: l10n.videoPlayerInApp, icon: Icons.play_circle_fill, payload: VideoPlayerMode.inApp), + ListPickerItem(label: l10n.linkHandlingCustomTabs, icon: Icons.language_rounded, payload: VideoPlayerMode.customTabs), + ListPickerItem(label: l10n.videoLinkHandlingExternal, icon: Icons.open_in_browser_rounded, payload: VideoPlayerMode.externalPlayer), + ], + leading: Icon(Icons.video_label_outlined), + onChanged: (value) => setPreferences(LocalSettings.videoPlayerMode, value.payload.name), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.videoPlayerMode), + highlighted: settingToHighlight == LocalSettings.videoPlayerMode), ], ), ], diff --git a/lib/src/features/settings/presentation/pages/debug_settings_page.dart b/lib/src/features/settings/presentation/pages/debug_settings_page.dart index 4082155e7..13ae094cf 100644 --- a/lib/src/features/settings/presentation/pages/debug_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/debug_settings_page.dart @@ -17,14 +17,14 @@ import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; import 'package:thunder/src/features/notification/notification.dart'; -import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; import 'package:unifiedpush/unifiedpush.dart'; -import 'package:thunder/packages/ui/ui.dart' show ListPickerItem, ThunderDivider, showSnackbar, showThunderDialog; +import 'package:thunder/packages/ui/ui.dart'; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; class DebugSettingsPage extends StatefulWidget { final LocalSettings? settingToHighlight; @@ -182,95 +182,92 @@ class _DebugSettingsPageState extends State { ], ), ), - SettingsListTile( - icon: Icons.co_present_rounded, - description: l10n.deleteLocalPreferences, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - showThunderDialog( - context: context, - title: l10n.deleteLocalPreferences, - contentText: l10n.deleteLocalPreferencesDescription, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - secondaryButtonText: l10n.cancel, - onPrimaryButtonPressed: (dialogContext, _) async { - final cleared = await UserPreferences.clearAllPreferences(); - - if (cleared) { - context.read().add(UserPreferencesChangeEvent()); - showSnackbar(AppLocalizations.of(context)!.clearedUserPreferences); - } else { - showSnackbar(AppLocalizations.of(context)!.failedToPerformAction); - } - - Navigator.of(dialogContext).pop(); - }, - primaryButtonText: l10n.clearPreferences, - ); - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.debugDeleteLocalPreferences, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.co_present_rounded), + title: l10n.deleteLocalPreferences, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + showThunderDialog( + context: context, + title: l10n.deleteLocalPreferences, + contentText: l10n.deleteLocalPreferencesDescription, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: l10n.cancel, + onPrimaryButtonPressed: (dialogContext, _) async { + final cleared = await UserPreferences.clearAllPreferences(); + + if (cleared) { + context.read().add(UserPreferencesChangeEvent()); + showSnackbar(AppLocalizations.of(context)!.clearedUserPreferences); + } else { + showSnackbar(AppLocalizations.of(context)!.failedToPerformAction); + } + + Navigator.of(dialogContext).pop(); + }, + primaryButtonText: l10n.clearPreferences, + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.debugDeleteLocalPreferences), + highlighted: settingToHighlight == LocalSettings.debugDeleteLocalPreferences), SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.data_array_rounded, - description: l10n.deleteLocalDatabase, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - showThunderDialog( - context: context, - title: l10n.deleteLocalDatabase, - contentText: l10n.deleteLocalDatabaseDescription, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - secondaryButtonText: l10n.cancel, - onPrimaryButtonPressed: (dialogContext, _) async { - String path = join(await getDatabasesPath(), 'thunder.db'); - - final dbFolder = await getApplicationDocumentsDirectory(); - final file = File(join(dbFolder.path, 'thunder.sqlite')); - - await databaseFactory.deleteDatabase(file.path); - - if (context.mounted) { - showSnackbar(AppLocalizations.of(context)!.clearedDatabase); - Navigator.of(context).pop(); - } - }, - primaryButtonText: l10n.clearDatabase, - ); - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.debugDeleteLocalDatabase, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.data_array_rounded), + title: l10n.deleteLocalDatabase, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + showThunderDialog( + context: context, + title: l10n.deleteLocalDatabase, + contentText: l10n.deleteLocalDatabaseDescription, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + secondaryButtonText: l10n.cancel, + onPrimaryButtonPressed: (dialogContext, _) async { + String path = join(await getDatabasesPath(), 'thunder.db'); + + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(join(dbFolder.path, 'thunder.sqlite')); + + await databaseFactory.deleteDatabase(file.path); + + if (context.mounted) { + showSnackbar(AppLocalizations.of(context)!.clearedDatabase); + Navigator.of(context).pop(); + } + }, + primaryButtonText: l10n.clearDatabase, + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.debugDeleteLocalDatabase), + highlighted: settingToHighlight == LocalSettings.debugDeleteLocalDatabase), const ThunderDivider(sliver: false), FutureBuilder( future: getExtendedImageCacheSize(), builder: (context, snapshot) { if (snapshot.hasData) { - return SettingsListTile( - icon: Icons.data_saver_off_rounded, - description: l10n.clearCache('${(snapshot.data! / (1024 * 1024)).toStringAsFixed(2)} MB'), - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () async { - await clearDiskCachedImages(); - if (context.mounted) showSnackbar(l10n.clearedCache); - setState(() {}); // Trigger a rebuild to refresh the cache size - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.debugClearCache, - highlightedSetting: settingToHighlight, - ); + return ThunderSettingsTile( + leading: Icon(Icons.data_saver_off_rounded), + title: l10n.clearCache('${(snapshot.data! / (1024 * 1024)).toStringAsFixed(2)} MB'), + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () async { + await clearDiskCachedImages(); + if (context.mounted) showSnackbar(l10n.clearedCache); + setState(() {}); // Trigger a rebuild to refresh the cache size + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.debugClearCache), + highlighted: settingToHighlight == LocalSettings.debugClearCache); } return Container(); }, @@ -298,56 +295,46 @@ class _DebugSettingsPageState extends State { children: [Text(l10n.status, style: theme.textTheme.titleSmall)], ), ), - SettingsListTile( - icon: Icons.info_rounded, - description: l10n.currentNotificationsMode(inboxNotificationType.toString()), - widget: Container(), - onTap: null, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), - SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.info_rounded, - description: l10n.areNotificationsAllowedBySystem(areNotificationsAllowed ? l10n.yes : l10n.no), - widget: Container(), - onTap: null, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), - if (!kIsWeb && Platform.isAndroid && enableExperimentalFeatures) ...[ - SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.info_rounded, - description: l10n.unifiedPushDistributorApp(unifiedPushDistributorApp ?? l10n.none, unifiedPushDistributorAppCount), - widget: Container(), + ThunderSettingsTile( + leading: Icon(Icons.info_rounded), + title: l10n.currentNotificationsMode(inboxNotificationType.toString()), + trailing: Container(), onTap: null, highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), - SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.info_rounded, - description: '${l10n.thunderNotificationServer(thunderNotificationServer ?? l10n.none)} ${pingDone ? '(${thunderNotificationServerPing ?? l10n.offline})' : ''}', - widget: Container(), + highlighted: false), + SizedBox(height: 8.0), + ThunderSettingsTile( + leading: Icon(Icons.info_rounded), + title: l10n.areNotificationsAllowedBySystem(areNotificationsAllowed ? l10n.yes : l10n.no), + trailing: Container(), onTap: null, highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + highlighted: false), + if (!kIsWeb && Platform.isAndroid && enableExperimentalFeatures) ...[ SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.info_rounded, - description: l10n.unifiedPushServer(unifiedPushServer ?? l10n.none), - widget: Container(), - onTap: null, - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.info_rounded), + title: l10n.unifiedPushDistributorApp(unifiedPushDistributorApp ?? l10n.none, unifiedPushDistributorAppCount), + trailing: Container(), + onTap: null, + highlightKey: settingToHighlightKey, + highlighted: false), + SizedBox(height: 8.0), + ThunderSettingsTile( + leading: Icon(Icons.info_rounded), + title: '${l10n.thunderNotificationServer(thunderNotificationServer ?? l10n.none)} ${pingDone ? '(${thunderNotificationServerPing ?? l10n.offline})' : ''}', + trailing: Container(), + onTap: null, + highlightKey: settingToHighlightKey, + highlighted: false), + SizedBox(height: 8.0), + ThunderSettingsTile( + leading: Icon(Icons.info_rounded), + title: l10n.unifiedPushServer(unifiedPushServer ?? l10n.none), + trailing: Container(), + onTap: null, + highlightKey: settingToHighlightKey, + highlighted: false), ], if (!kIsWeb && Platform.isAndroid) ...[ Padding( @@ -357,109 +344,37 @@ class _DebugSettingsPageState extends State { children: [Text(l10n.localNotifications, style: theme.textTheme.titleSmall)], ), ), - SettingsListTile( - icon: Icons.notifications_rounded, - description: l10n.sendTestLocalNotification, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: inboxNotificationType == NotificationType.local - ? () { - showTestAndroidNotification(); - } - : null, - highlightKey: settingToHighlightKey, - setting: LocalSettings.debugSendTestLocalNotification, - highlightedSetting: settingToHighlight, - ), - SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.circle_notifications_rounded, - description: l10n.sendBackgroundTestLocalNotification, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: inboxNotificationType == NotificationType.local - ? () async { - bool result = false; - - await showThunderDialog( - context: context, - title: l10n.confirm, - contentWidgetBuilder: (setPrimaryButtonEnabled) => Text(l10n.testBackgroundNotificationDescription), - primaryButtonText: l10n.confirm, - primaryButtonInitialEnabled: true, - onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { - Navigator.of(dialogContext).pop(); - result = true; - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - ); - - if (result) { - // Hook up a callback to generate a background notification. - // The next time Thunder starts, this will get reset - await disableBackgroundFetch(); - await initTestBackgroundFetch(); - initTestHeadlessBackgroundFetch(); - - SystemNavigator.pop(); - } - } - : null, - highlightKey: settingToHighlightKey, - setting: LocalSettings.debugSendBackgroundTestLocalNotification, - highlightedSetting: settingToHighlight, - ), - if (enableExperimentalFeatures) ...[ - Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 6.0, bottom: 6.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [Text(l10n.unifiedpush, style: theme.textTheme.titleSmall)], - ), - ), - SettingsListTile( - icon: Icons.notifications_rounded, - description: l10n.sendTestUnifiedPushNotification, - widget: const SizedBox( + ThunderSettingsTile( + leading: Icon(Icons.notifications_rounded), + title: l10n.sendTestLocalNotification, + trailing: const SizedBox( height: 42.0, child: Icon(Icons.chevron_right_rounded), ), - onTap: inboxNotificationType == NotificationType.unifiedPush - ? () async { - final error = await requestTestNotification(); - - if (error == null) { - showSnackbar(l10n.sentRequestForTestNotification); - } else { - showSnackbar(l10n.failedToCommunicateWithThunderNotificationServer('$pushNotificationServer\n\n$error')); - } + onTap: inboxNotificationType == NotificationType.local + ? () { + showTestAndroidNotification(); } : null, highlightKey: settingToHighlightKey, - setting: LocalSettings.debugSendTestUnifiedPushNotification, - highlightedSetting: settingToHighlight, - ), - SizedBox(height: 8.0), - SettingsListTile( - icon: Icons.circle_notifications_rounded, - description: l10n.sendBackgroundTestUnifiedPushNotification, - widget: const SizedBox( + onLongPress: () => shareLocalSetting(context, LocalSettings.debugSendTestLocalNotification), + highlighted: settingToHighlight == LocalSettings.debugSendTestLocalNotification), + SizedBox(height: 8.0), + ThunderSettingsTile( + leading: Icon(Icons.circle_notifications_rounded), + title: l10n.sendBackgroundTestLocalNotification, + trailing: const SizedBox( height: 42.0, child: Icon(Icons.chevron_right_rounded), ), - onTap: inboxNotificationType == NotificationType.unifiedPush + onTap: inboxNotificationType == NotificationType.local ? () async { bool result = false; await showThunderDialog( context: context, title: l10n.confirm, - contentWidgetBuilder: (setPrimaryButtonEnabled) => Text(l10n.testBackgroundUnifiedPushNotificationDescription), + contentWidgetBuilder: (setPrimaryButtonEnabled) => Text(l10n.testBackgroundNotificationDescription), primaryButtonText: l10n.confirm, primaryButtonInitialEnabled: true, onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { @@ -471,6 +386,36 @@ class _DebugSettingsPageState extends State { ); if (result) { + // Hook up a callback to generate a background notification. + // The next time Thunder starts, this will get reset + await disableBackgroundFetch(); + await initTestBackgroundFetch(); + initTestHeadlessBackgroundFetch(); + + SystemNavigator.pop(); + } + } + : null, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.debugSendBackgroundTestLocalNotification), + highlighted: settingToHighlight == LocalSettings.debugSendBackgroundTestLocalNotification), + if (enableExperimentalFeatures) ...[ + Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 6.0, bottom: 6.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(l10n.unifiedpush, style: theme.textTheme.titleSmall)], + ), + ), + ThunderSettingsTile( + leading: Icon(Icons.notifications_rounded), + title: l10n.sendTestUnifiedPushNotification, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: inboxNotificationType == NotificationType.unifiedPush + ? () async { final error = await requestTestNotification(); if (error == null) { @@ -478,30 +423,66 @@ class _DebugSettingsPageState extends State { } else { showSnackbar(l10n.failedToCommunicateWithThunderNotificationServer('$pushNotificationServer\n\n$error')); } - - SystemNavigator.pop(); } - } - : null, - highlightKey: settingToHighlightKey, - setting: LocalSettings.debugSendBackgroundTestUnifiedPushNotification, - highlightedSetting: settingToHighlight, - ), + : null, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.debugSendTestUnifiedPushNotification), + highlighted: settingToHighlight == LocalSettings.debugSendTestUnifiedPushNotification), + SizedBox(height: 8.0), + ThunderSettingsTile( + leading: Icon(Icons.circle_notifications_rounded), + title: l10n.sendBackgroundTestUnifiedPushNotification, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: inboxNotificationType == NotificationType.unifiedPush + ? () async { + bool result = false; + + await showThunderDialog( + context: context, + title: l10n.confirm, + contentWidgetBuilder: (setPrimaryButtonEnabled) => Text(l10n.testBackgroundUnifiedPushNotificationDescription), + primaryButtonText: l10n.confirm, + primaryButtonInitialEnabled: true, + onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) { + Navigator.of(dialogContext).pop(); + result = true; + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + + if (result) { + final error = await requestTestNotification(); + + if (error == null) { + showSnackbar(l10n.sentRequestForTestNotification); + } else { + showSnackbar(l10n.failedToCommunicateWithThunderNotificationServer('$pushNotificationServer\n\n$error')); + } + + SystemNavigator.pop(); + } + } + : null, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.debugSendBackgroundTestUnifiedPushNotification), + highlighted: settingToHighlight == LocalSettings.debugSendBackgroundTestUnifiedPushNotification), ], ], const ThunderDivider(sliver: false), - SettingsListTile( - icon: Icons.edit_notifications_rounded, - description: l10n.changeNotificationSettings, - widget: const SizedBox( - height: 42.0, - child: Icon(Icons.chevron_right_rounded), - ), - onTap: () => navigateToSettingPage(context, LocalSettings.inboxNotificationType), - highlightKey: settingToHighlightKey, - setting: null, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.edit_notifications_rounded), + title: l10n.changeNotificationSettings, + trailing: const SizedBox( + height: 42.0, + child: Icon(Icons.chevron_right_rounded), + ), + onTap: () => navigateToSettingPage(context, LocalSettings.inboxNotificationType), + highlightKey: settingToHighlightKey, + highlighted: false), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), child: Column( @@ -519,31 +500,29 @@ class _DebugSettingsPageState extends State { ), ), SizedBox(height: 8.0), - ToggleOption( - description: l10n.enableExperimentalFeatures, - value: enableExperimentalFeatures, - iconEnabled: Icons.construction_rounded, - iconDisabled: Icons.construction_outlined, - onToggle: (value) => setPreferences(LocalSettings.enableExperimentalFeatures, value), - highlightKey: settingToHighlightKey, - setting: LocalSettings.enableExperimentalFeatures, - highlightedSetting: settingToHighlight, - ), + ThunderToggleOption( + title: l10n.enableExperimentalFeatures, + value: enableExperimentalFeatures, + iconEnabled: Icons.construction_rounded, + iconDisabled: Icons.construction_outlined, + onChanged: (value) => setPreferences(LocalSettings.enableExperimentalFeatures, value), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.enableExperimentalFeatures), + highlighted: settingToHighlight == LocalSettings.enableExperimentalFeatures), Padding( padding: const EdgeInsets.only(left: 16.0, right: 16.0, top: 16.0), child: Text(l10n.feed, style: theme.textTheme.titleMedium), ), SizedBox(height: 8.0), - ListOption( - description: l10n.imageDimensionTimeout, - value: ListPickerItem(label: '${imageDimensionTimeout}s', icon: Icons.timelapse, payload: imageDimensionTimeout), - options: imageDimensionTimeouts.map((value) => ListPickerItem(icon: Icons.timelapse, label: '${value}s', payload: value)).toList(), - icon: Icons.timelapse, - onChanged: (value) async => setPreferences(LocalSettings.imageDimensionTimeout, value.payload), - highlightKey: settingToHighlightKey, - setting: LocalSettings.imageDimensionTimeout, - highlightedSetting: settingToHighlight, - ), + ThunderListOption( + title: l10n.imageDimensionTimeout, + value: ListPickerItem(label: '${imageDimensionTimeout}s', icon: Icons.timelapse, payload: imageDimensionTimeout), + options: imageDimensionTimeouts.map((value) => ListPickerItem(icon: Icons.timelapse, label: '${value}s', payload: value)).toList(), + leading: Icon(Icons.timelapse), + onChanged: (value) async => setPreferences(LocalSettings.imageDimensionTimeout, value.payload), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.imageDimensionTimeout), + highlighted: settingToHighlight == LocalSettings.imageDimensionTimeout), SizedBox(height: 48), ], ), diff --git a/lib/src/features/settings/presentation/pages/user_labels_settings_page.dart b/lib/src/features/settings/presentation/pages/user_labels_settings_page.dart index 11f794de9..964b65efc 100644 --- a/lib/src/features/settings/presentation/pages/user_labels_settings_page.dart +++ b/lib/src/features/settings/presentation/pages/user_labels_settings_page.dart @@ -11,7 +11,7 @@ import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -153,12 +153,10 @@ class _UserLabelSettingsPageState extends State with Sing return ListTile( contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), title: UserFullNameWidget( - context, - UserLabel.partsFromUsername(userLabels[index].username).username, - null, - UserLabel.partsFromUsername(userLabels[index].username).instance, - textStyle: theme.textTheme.bodyLarge, - ), + name: UserLabel.partsFromUsername(userLabels[index].username).username, + displayName: null, + instance: UserLabel.partsFromUsername(userLabels[index].username).instance, + textStyle: theme.textTheme.bodyLarge), subtitle: Text(userLabels[index].label), trailing: IconButton( icon: Icon(Icons.clear, semanticLabel: l10n.remove), diff --git a/lib/src/features/settings/presentation/utils/setting_link_utils.dart b/lib/src/features/settings/presentation/utils/setting_link_utils.dart index 9eae9fa28..b60c6f184 100644 --- a/lib/src/features/settings/presentation/utils/setting_link_utils.dart +++ b/lib/src/features/settings/presentation/utils/setting_link_utils.dart @@ -18,3 +18,10 @@ void shareSetting(BuildContext context, LocalSettings? setting, String descripti Clipboard.setData(ClipboardData(text: '[Thunder Setting: $path](thunder://setting-${setting.name})')); showSnackbar('Setting link copied to clipboard!'); } + +void shareLocalSetting(BuildContext context, LocalSettings setting) { + final l10n = AppLocalizations.of(context)!; + final description = l10n.getLocalSettingLocalization(setting.key); + + shareSetting(context, setting, description); +} diff --git a/lib/src/features/settings/presentation/widgets/accessibility_profile.dart b/lib/src/features/settings/presentation/widgets/accessibility_profile.dart index 85d9f0222..5ff1fe564 100644 --- a/lib/src/features/settings/presentation/widgets/accessibility_profile.dart +++ b/lib/src/features/settings/presentation/widgets/accessibility_profile.dart @@ -6,10 +6,9 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/features/settings/settings.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/packages/ui/ui.dart' show showSnackbar; +import 'package:thunder/packages/ui/ui.dart'; class SettingProfile extends StatelessWidget { final IconData icon; @@ -31,9 +30,9 @@ class SettingProfile extends StatelessWidget { final ThemeData theme = Theme.of(context); bool recentSuccess = false; - return ExpandableOption( - icon: icon, - description: name, + return ThunderExpandableOption( + icon: Icon(icon), + title: name, child: Column( children: [ Text(description), diff --git a/lib/src/features/settings/presentation/widgets/action_color_setting_widget.dart b/lib/src/features/settings/presentation/widgets/action_color_setting_widget.dart index 5cea6014d..c508b47df 100644 --- a/lib/src/features/settings/presentation/widgets/action_color_setting_widget.dart +++ b/lib/src/features/settings/presentation/widgets/action_color_setting_widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/settings/settings.dart'; -import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, ListPickerItem; +import 'package:thunder/packages/ui/ui.dart'; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; class ActionColorSettingWidget extends StatelessWidget { final LocalSettings? settingToHighlight; @@ -51,282 +51,282 @@ class ActionColorSettingWidget extends StatelessWidget { padding: const EdgeInsets.only(left: 16.0, bottom: 8.0), child: Text(l10n.colors, style: theme.textTheme.titleLarge), ), - ListOption( - isBottomModalScrollControlled: true, - value: const ListPickerItem(payload: -1), - description: l10n.actionColors, - icon: Icons.color_lens_rounded, - highlightKey: settingToHighlightKey, - setting: LocalSettings.actionColors, - highlightedSetting: settingToHighlight, - customListPicker: StatefulBuilder( - builder: (context, setState) { - return BottomSheetListPicker( - title: l10n.actionColors, - items: [ - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.upvoteColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: upvoteColor, - items: ActionColor.getPossibleValues(upvoteColor) - .map( - (actionColor) => DropdownMenuItem( - alignment: Alignment.center, - value: actionColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: actionColor.color, - ), - const SizedBox(width: 16.0), - Text( - actionColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ThunderListOption( + isBottomModalScrollControlled: true, + value: const ListPickerItem(payload: -1), + options: const [], + title: l10n.actionColors, + leading: Icon(Icons.color_lens_rounded), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.actionColors), + highlighted: settingToHighlight == LocalSettings.actionColors, + customListPicker: StatefulBuilder( + builder: (context, setState) { + return BottomSheetListPicker( + title: l10n.actionColors, + items: [ + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.upvoteColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: upvoteColor, + items: ActionColor.getPossibleValues(upvoteColor) + .map( + (actionColor) => DropdownMenuItem( + alignment: Alignment.center, + value: actionColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: actionColor.color, + ), + const SizedBox(width: 16.0), + Text( + actionColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.upvoteColor, value?.colorRaw); - setState(() => upvoteColor = value ?? upvoteColor); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.upvoteColor, value?.colorRaw); + setState(() => upvoteColor = value ?? upvoteColor); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.downvoteColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: downvoteColor, - items: ActionColor.getPossibleValues(downvoteColor) - .map( - (actionColor) => DropdownMenuItem( - alignment: Alignment.center, - value: actionColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: actionColor.color, - ), - const SizedBox(width: 16.0), - Text( - actionColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.downvoteColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: downvoteColor, + items: ActionColor.getPossibleValues(downvoteColor) + .map( + (actionColor) => DropdownMenuItem( + alignment: Alignment.center, + value: actionColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: actionColor.color, + ), + const SizedBox(width: 16.0), + Text( + actionColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.downvoteColor, value?.colorRaw); - setState(() => downvoteColor = value ?? downvoteColor); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.downvoteColor, value?.colorRaw); + setState(() => downvoteColor = value ?? downvoteColor); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.saveColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: saveColor, - items: ActionColor.getPossibleValues(saveColor) - .map( - (actionColor) => DropdownMenuItem( - alignment: Alignment.center, - value: actionColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: actionColor.color, - ), - const SizedBox(width: 16.0), - Text( - actionColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.saveColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: saveColor, + items: ActionColor.getPossibleValues(saveColor) + .map( + (actionColor) => DropdownMenuItem( + alignment: Alignment.center, + value: actionColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: actionColor.color, + ), + const SizedBox(width: 16.0), + Text( + actionColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.saveColor, value?.colorRaw); - setState(() => saveColor = value ?? saveColor); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.saveColor, value?.colorRaw); + setState(() => saveColor = value ?? saveColor); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.markReadColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: markReadColor, - items: ActionColor.getPossibleValues(markReadColor) - .map( - (actionColor) => DropdownMenuItem( - alignment: Alignment.center, - value: actionColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: actionColor.color, - ), - const SizedBox(width: 16.0), - Text( - actionColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.markReadColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: markReadColor, + items: ActionColor.getPossibleValues(markReadColor) + .map( + (actionColor) => DropdownMenuItem( + alignment: Alignment.center, + value: actionColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: actionColor.color, + ), + const SizedBox(width: 16.0), + Text( + actionColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.markReadColor, value?.colorRaw); - setState(() => markReadColor = value ?? markReadColor); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.markReadColor, value?.colorRaw); + setState(() => markReadColor = value ?? markReadColor); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.replyColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: replyColor, - items: ActionColor.getPossibleValues(replyColor) - .map( - (actionColor) => DropdownMenuItem( - alignment: Alignment.center, - value: actionColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: actionColor.color, - ), - const SizedBox(width: 16.0), - Text( - actionColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.replyColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: replyColor, + items: ActionColor.getPossibleValues(replyColor) + .map( + (actionColor) => DropdownMenuItem( + alignment: Alignment.center, + value: actionColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: actionColor.color, + ), + const SizedBox(width: 16.0), + Text( + actionColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.replyColor, value?.colorRaw); - setState(() => replyColor = value ?? replyColor); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.replyColor, value?.colorRaw); + setState(() => replyColor = value ?? replyColor); + }, + ), ), ), ), - ), - ListPickerItem( - payload: -1, - customWidget: ListTile( - title: Text( - l10n.hideColor, - style: theme.textTheme.bodyMedium, - ), - subtitle: Padding( - padding: const EdgeInsets.only(left: 16.0, right: 16.0), - child: DropdownButton( - borderRadius: const BorderRadius.all(Radius.circular(10.0)), - isExpanded: true, - underline: Container(), - value: hideColor, - items: ActionColor.getPossibleValues(hideColor) - .map( - (actionColor) => DropdownMenuItem( - alignment: Alignment.center, - value: actionColor, - child: Row( - children: [ - CircleAvatar( - radius: 10.0, - backgroundColor: actionColor.color, - ), - const SizedBox(width: 16.0), - Text( - actionColor.label(context), - style: theme.textTheme.bodyMedium, - ), - ], + ListPickerItem( + payload: -1, + customWidget: ListTile( + title: Text( + l10n.hideColor, + style: theme.textTheme.bodyMedium, + ), + subtitle: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: DropdownButton( + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + isExpanded: true, + underline: Container(), + value: hideColor, + items: ActionColor.getPossibleValues(hideColor) + .map( + (actionColor) => DropdownMenuItem( + alignment: Alignment.center, + value: actionColor, + child: Row( + children: [ + CircleAvatar( + radius: 10.0, + backgroundColor: actionColor.color, + ), + const SizedBox(width: 16.0), + Text( + actionColor.label(context), + style: theme.textTheme.bodyMedium, + ), + ], + ), ), - ), - ) - .toList(), - onChanged: (value) async { - await setPreferences(LocalSettings.hideColor, value?.colorRaw); - setState(() => hideColor = value ?? hideColor); - }, + ) + .toList(), + onChanged: (value) async { + await setPreferences(LocalSettings.hideColor, value?.colorRaw); + setState(() => hideColor = value ?? hideColor); + }, + ), ), ), ), - ), - ], - ); - }, - ), - ), + ], + ); + }, + )), ], ), ); diff --git a/lib/src/features/settings/presentation/widgets/expandable_option.dart b/lib/src/features/settings/presentation/widgets/expandable_option.dart deleted file mode 100644 index f18bfc093..000000000 --- a/lib/src/features/settings/presentation/widgets/expandable_option.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:flutter/material.dart'; - -/// This widget creates an option which can be expanded to see the full contents -class ExpandableOption extends StatefulWidget { - final IconData? icon; - final String description; - final Widget child; - - const ExpandableOption({ - super.key, - this.icon, - required this.description, - required this.child, - }); - - @override - State createState() => _ExpandableOptionState(); -} - -class _ExpandableOptionState extends State with SingleTickerProviderStateMixin { - bool isExpanded = false; - - late final AnimationController _controller = AnimationController( - duration: const Duration(milliseconds: 100), - vsync: this, - ); - - // Animation for settings collapse - late final Animation _offsetAnimation = Tween( - begin: Offset.zero, - end: const Offset(1.5, 0.0), - ).animate(CurvedAnimation( - parent: _controller, - curve: Curves.fastOutSlowIn, - )); - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - - return Column( - children: [ - InkWell( - borderRadius: const BorderRadius.all(Radius.circular(50)), - onTap: () => setState(() => isExpanded = !isExpanded), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon(widget.icon), - const SizedBox(width: 8.0), - Text(widget.description, style: theme.textTheme.bodyMedium), - ], - ), - const SizedBox( - height: 40, - ), - Icon(isExpanded ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded), - ], - ), - ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - transitionBuilder: (Widget child, Animation animation) { - return SizeTransition( - sizeFactor: animation, - child: SlideTransition(position: _offsetAnimation, child: child), - ); - }, - child: isExpanded - ? Padding( - padding: const EdgeInsets.all(6.0), - child: widget.child, - ) - : Container(), - ), - ], - ); - } -} diff --git a/lib/src/features/settings/presentation/widgets/list_option.dart b/lib/src/features/settings/presentation/widgets/list_option.dart deleted file mode 100644 index 42b8d0ae1..000000000 --- a/lib/src/features/settings/presentation/widgets/list_option.dart +++ /dev/null @@ -1,147 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:flex_color_scheme/flex_color_scheme.dart'; -import 'package:smooth_highlight/smooth_highlight.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/settings/settings.dart'; -import 'package:thunder/packages/ui/ui.dart' show BottomSheetListPicker, ListPickerItem; - -class ListOption extends StatelessWidget { - // Appearance - final IconData? icon; - - // General - final String description; - final String? subtitle; - final Widget? subtitleWidget; - final Widget? bottomSheetHeading; - final ListPickerItem value; - final List> options; - - // Callback - final Future Function(ListPickerItem)? onChanged; - - final Widget? customListPicker; - final bool? isBottomModalScrollControlled; - - final bool disabled; - final Widget? valueDisplay; - final bool closeOnSelect; - final Widget Function()? onUpdateHeading; - - /// A key to assign to this widget when it should be highlighted - final GlobalKey? highlightKey; - - /// The setting that this widget controls. - final LocalSettings? setting; - - /// The highlighted setting, if any. - final LocalSettings? highlightedSetting; - - const ListOption({ - super.key, - this.description = '', - this.subtitle, - this.subtitleWidget, - this.bottomSheetHeading, - required this.value, - this.options = const [], - this.icon, - this.onChanged, - this.customListPicker, - this.isBottomModalScrollControlled, - this.disabled = false, - this.valueDisplay, - this.closeOnSelect = true, - this.onUpdateHeading, - this.highlightKey, - this.setting, - this.highlightedSetting, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return SmoothHighlight( - key: highlightedSetting == setting && setting != null ? highlightKey : null, - useInitialHighLight: highlightedSetting == setting && setting != null, - enabled: highlightedSetting == setting && setting != null, - color: theme.colorScheme.primaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(50)), - onTap: disabled - ? null - : () { - showModalBottomSheet( - context: context, - showDragHandle: true, - isScrollControlled: isBottomModalScrollControlled ?? false, - builder: (context) => - customListPicker ?? - BottomSheetListPicker( - title: description, - heading: bottomSheetHeading, - onUpdateHeading: onUpdateHeading, - items: options, - onSelect: onChanged ?? (value) async {}, - previouslySelected: value.payload, - closeOnSelect: closeOnSelect, - ), - ); - }, - onLongPress: disabled ? null : () => shareSetting(context, setting, description), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Icon(icon), - const SizedBox(width: 8.0), - ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width - 140), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(description, style: theme.textTheme.bodyMedium), - if (subtitleWidget != null) subtitleWidget!, - if (subtitle != null) Text(subtitle!, style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.8))), - ], - ), - ), - ], - ), - Row( - children: [ - valueDisplay ?? - Text( - value.capitalizeLabel - ? value.label.capitalize.replaceAll('_', '').replaceAll(' ', '').replaceAllMapped(RegExp(r'([A-Z])'), (match) { - return ' ${match.group(0)}'; - }) - : value.label, - style: theme.textTheme.titleSmall?.copyWith( - color: disabled ? theme.colorScheme.onSurface.withValues(alpha: 0.5) : theme.colorScheme.onSurface, - ), - ), - Icon( - Icons.chevron_right_rounded, - color: disabled ? theme.colorScheme.onSurface.withValues(alpha: 0.5) : null, - ), - const SizedBox( - height: 42.0, - ) - ], - ) - ], - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/features/settings/presentation/widgets/settings_list_tile.dart b/lib/src/features/settings/presentation/widgets/settings_list_tile.dart deleted file mode 100644 index cf062139b..000000000 --- a/lib/src/features/settings/presentation/widgets/settings_list_tile.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:smooth_highlight/smooth_highlight.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/settings/settings.dart'; - -class SettingsListTile extends StatelessWidget { - // Appearance - final IconData? icon; - - // General - final String description; - final String? subtitle; - final String? semanticLabel; - final int? subtitleMaxLines; - - // Callback - final Function()? onTap; - final Function()? onLongPress; - - final Widget widget; - - /// A key to assign to this widget when it should be highlighted - final GlobalKey? highlightKey; - - /// The setting that this widget controls. - final LocalSettings? setting; - - /// The highlighted setting, if any. - final LocalSettings? highlightedSetting; - - const SettingsListTile({ - super.key, - required this.description, - this.subtitle, - this.semanticLabel, - this.subtitleMaxLines, - required this.widget, - this.icon, - this.onTap, - this.onLongPress, - required this.highlightKey, - required this.setting, - required this.highlightedSetting, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return SmoothHighlight( - key: highlightedSetting == setting && setting != null ? highlightKey : null, - useInitialHighLight: highlightedSetting == setting && setting != null, - enabled: highlightedSetting == setting && setting != null, - color: theme.colorScheme.primaryContainer, - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 0.0), - child: Semantics( - label: semanticLabel ?? description, - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(50)), - onTap: onTap, - onLongPress: onLongPress ?? (onTap == null ? null : () => shareSetting(context, setting, description)), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (icon != null) Icon(icon), - const SizedBox(width: 8.0), - Column( - children: [ - ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width - 140), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Semantics( - // We will set semantics at the top widget level - // rather than having the Text widget read automatically - excludeSemantics: true, - child: Text( - description, - style: onTap != null || onLongPress != null - ? theme.textTheme.bodyMedium - : theme.textTheme.bodyMedium?.copyWith( - color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.5), - ), - ), - ), - if (subtitle != null) - Text( - subtitle!, - maxLines: subtitleMaxLines, - style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.8)), - ), - ], - ), - ), - ], - ), - ], - ), - Container( - child: widget, - ) - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/features/settings/presentation/widgets/toggle_option.dart b/lib/src/features/settings/presentation/widgets/toggle_option.dart deleted file mode 100644 index aeb980d08..000000000 --- a/lib/src/features/settings/presentation/widgets/toggle_option.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:smooth_highlight/smooth_highlight.dart'; -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/settings/settings.dart'; - -class ToggleOption extends StatelessWidget { - /// The icon to display when enabled - final IconData? iconEnabled; - - /// A custom icon size for the enabled icon if provided - final double? iconEnabledSize; - - /// The icon to display when disabled - final IconData? iconDisabled; - - /// A custom icon size for the disabled icon if provided - final double? iconDisabledSize; - - /// The spacing between the icon and the label. Defaults to 8.0 - final double? iconSpacing; - - /// The main label for the ToggleOption - final String description; - - /// An optional subtitle shown below the description - final String? subtitle; - - /// A custom semantic label for the option - final String? semanticLabel; - - /// The value of the option. - /// When null, the [Switch] will be hidden and the [onToggle] callback will be ignored. - /// When null, the [onTap] and [onLongPress] callbacks are still available. - final bool? value; - - /// A callback function to perform when the option is toggled. - /// When null, the [ToggleOption] is non-interactable. No callback functions will be activated. - final Function(bool)? onToggle; - - /// A callback function to perform when the option is tapped. - /// If null, tapping will toggle the [Switch] and trigger the [onToggle] callback. - final Function()? onTap; - - /// A callback function to perform when the option is long pressed - final Function()? onLongPress; - - final List? additionalWidgets; - - /// Override the default padding - final EdgeInsets? padding; - - /// A key to assign to this widget when it should be highlighted - final GlobalKey? highlightKey; - - /// The setting that this widget controls. - final LocalSettings? setting; - - /// The highlighted setting, if any. - final LocalSettings? highlightedSetting; - - /// Whether this setting can be changed by the user or not - final bool disabled; - - const ToggleOption({ - super.key, - required this.description, - this.subtitle, - this.semanticLabel, - required this.value, - this.iconEnabled, - this.iconEnabledSize, - this.iconDisabled, - this.iconDisabledSize, - this.iconSpacing, - this.onToggle, - this.additionalWidgets, - this.onTap, - this.onLongPress, - this.padding, - required this.setting, - required this.highlightedSetting, - required this.highlightKey, - this.disabled = false, - }); - - void onTapInkWell() { - if (onTap == null && value != null) { - onToggle?.call(!value!); - } - - onTap?.call(); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return SmoothHighlight( - key: highlightedSetting == setting && setting != null ? highlightKey : null, - useInitialHighLight: highlightedSetting == setting && setting != null, - enabled: highlightedSetting == setting && setting != null, - color: theme.colorScheme.primaryContainer, - child: Padding( - padding: padding ?? const EdgeInsets.symmetric(horizontal: 16.0), - child: Semantics( - label: semanticLabel ?? description, - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(50)), - onTap: disabled - ? null - : onToggle == null - ? null - : onTapInkWell, - onLongPress: disabled - ? null - : onToggle == null - ? null - : onLongPress ?? () => shareSetting(context, setting, description), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - if (iconEnabled != null && iconDisabled != null) Icon(value == true ? iconEnabled : iconDisabled, size: value == true ? iconEnabledSize : iconDisabledSize), - if (iconEnabled != null && iconDisabled != null) SizedBox(width: iconSpacing ?? 8.0), - Column( - children: [ - ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width - 140), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Semantics( - // We will set semantics at the top widget level - // rather than having the Text widget read automatically - excludeSemantics: true, - child: Text( - description, - style: theme.textTheme.bodyMedium, - ), - ), - if (subtitle != null) Text(subtitle!, style: theme.textTheme.bodySmall?.copyWith(color: theme.textTheme.bodySmall?.color?.withValues(alpha: 0.8))), - ], - ), - ), - ], - ), - ], - ), - if (additionalWidgets?.isNotEmpty == true) ...[ - Expanded( - child: Container(), - ), - ...additionalWidgets!, - const SizedBox( - width: 20, - ), - ], - if (value != null) - Switch( - value: value!, - onChanged: disabled - ? null - : onToggle == null - ? null - : (bool value) { - HapticFeedback.lightImpact(); - onToggle?.call(value); - }, - ), - if (value == null) - const SizedBox( - height: 50, - width: 60, - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/src/features/settings/presentation/widgets/widgets.dart b/lib/src/features/settings/presentation/widgets/widgets.dart index 0ab11c7f2..9d85361a0 100644 --- a/lib/src/features/settings/presentation/widgets/widgets.dart +++ b/lib/src/features/settings/presentation/widgets/widgets.dart @@ -1,9 +1,5 @@ export 'accessibility_profile.dart'; export 'action_color_setting_widget.dart'; export 'discussion_language_selector.dart'; -export 'expandable_option.dart'; -export 'list_option.dart'; export 'post_placeholder.dart'; -export 'settings_list_tile.dart'; export 'swipe_picker.dart'; -export 'toggle_option.dart'; diff --git a/lib/src/features/user/presentation/pages/media_management_page.dart b/lib/src/features/user/presentation/pages/media_management_page.dart index 9ec35fa2c..3223b7d14 100644 --- a/lib/src/features/user/presentation/pages/media_management_page.dart +++ b/lib/src/features/user/presentation/pages/media_management_page.dart @@ -11,14 +11,14 @@ import 'package:thunder/src/features/comment/comment.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/config/config.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_utils.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; import 'package:thunder/packages/ui/ui.dart' show showSnackbar, showThunderDialog; class MediaManagementPage extends StatelessWidget { @@ -59,11 +59,9 @@ class MediaManagementPage extends StatelessWidget { style: theme.textTheme.titleLarge, ), subtitle: UserFullNameWidget( - context, - context.read().state.account.username, - context.read().state.account.displayName, - context.read().state.account.instance, - ), + name: context.read().state.account.username, + displayName: context.read().state.account.displayName, + instance: context.read().state.account.instance), contentPadding: const EdgeInsets.symmetric(horizontal: 0), ), ), @@ -224,7 +222,7 @@ class MediaManagementPage extends StatelessWidget { l10n.noReferencesToImage, textAlign: TextAlign.center, style: theme.textTheme.titleSmall, - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, ), ), ), @@ -287,7 +285,7 @@ class MediaManagementPage extends StatelessWidget { l10n.noImages, textAlign: TextAlign.center, style: theme.textTheme.titleSmall, - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, ), ), ), diff --git a/lib/src/features/user/presentation/pages/user_settings_block_page.dart b/lib/src/features/user/presentation/pages/user_settings_block_page.dart index 02b0acd2b..0cb5cc140 100644 --- a/lib/src/features/user/presentation/pages/user_settings_block_page.dart +++ b/lib/src/features/user/presentation/pages/user_settings_block_page.dart @@ -10,9 +10,9 @@ import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/config/config.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/shared/input_dialogs.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; @@ -59,11 +59,10 @@ class _UserSettingsBlockPageState extends State with Sing contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), title: Text(person.displayName ?? person.name, overflow: TextOverflow.ellipsis), subtitle: UserFullNameWidget( - context, - person.name, - person.displayName, - fetchInstanceNameFromUrl(person.actorId) ?? '-', - // Override because we're showing display name above + name: person.name, + displayName: person.displayName, + instance: fetchInstanceNameFromUrl(person.actorId) ?? '-', +// Override because we're showing display name above useDisplayName: false, ), leading: UserAvatar(user: person), @@ -104,10 +103,9 @@ class _UserSettingsBlockPageState extends State with Sing contentPadding: const EdgeInsetsDirectional.only(start: 16.0, end: 12.0), title: Text(community.title, overflow: TextOverflow.ellipsis), subtitle: CommunityFullNameWidget( - context, - community.title, - community.title, - fetchInstanceNameFromUrl(community.actorId) ?? '-', + name: community.title, + displayName: community.title, + instance: fetchInstanceNameFromUrl(community.actorId) ?? '-', // Override because we're showing display name above useDisplayName: false, ), diff --git a/lib/src/features/user/presentation/pages/user_settings_page.dart b/lib/src/features/user/presentation/pages/user_settings_page.dart index 137aada89..c63c58aa8 100644 --- a/lib/src/features/user/presentation/pages/user_settings_page.dart +++ b/lib/src/features/user/presentation/pages/user_settings_page.dart @@ -16,15 +16,15 @@ import "package:thunder/src/foundation/primitives/models/thunder_site_response.d import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/account.dart'; import "package:thunder/src/foundation/primitives/enums/enums.dart"; -import "package:thunder/src/features/settings/settings.dart"; import "package:thunder/src/shared/sort_picker.dart"; import "package:thunder/src/features/user/user.dart"; import "package:thunder/src/foundation/config/app_constants.dart"; import "package:thunder/src/foundation/networking/error_message_utils.dart"; import "package:thunder/src/foundation/config/global_context.dart"; import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; +import 'package:thunder/src/features/settings/presentation/utils/setting_link_utils.dart'; import "package:thunder/src/app/shell/navigation/navigation_utils.dart"; -import 'package:thunder/packages/ui/ui.dart' show ListPickerItem, Thunder, showSnackbar, showThunderDialog; +import 'package:thunder/packages/ui/ui.dart'; /// A widget that displays the user's account settings. These settings are synchronized with the instance and should be preferred over the app settings. class UserSettingsPage extends StatefulWidget { @@ -175,122 +175,118 @@ class _UserSettingsPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.general, style: theme.textTheme.titleMedium), ), - SettingsListTile( - icon: Icons.person_rounded, - description: l10n.displayName, - subtitle: person?.displayName?.isNotEmpty == true ? person?.displayName : l10n.noDisplayNameSet, - widget: const Padding(padding: EdgeInsets.all(20.0)), - onTap: () { - displayNameTextController.text = person?.displayName ?? ""; - showThunderDialog( - context: context, - title: l10n.displayName, - contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( - controller: displayNameTextController, - decoration: InputDecoration(hintText: l10n.displayName), - ), - primaryButtonText: l10n.save, - onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(displayName: displayNameTextController.text)); - Navigator.of(dialogContext).pop(); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - ); - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountDisplayName, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.note_rounded, - description: l10n.profileBio, - subtitle: person?.bio?.isNotEmpty == true ? parse(markdownToHtml(person?.bio ?? "")).documentElement?.text.trim() : l10n.noProfileBioSet, - subtitleMaxLines: 1, - widget: const Padding(padding: EdgeInsets.all(20.0)), - onTap: () { - bioTextController.text = person?.bio ?? ""; - showThunderDialog( - context: context, - title: l10n.profileBio, - contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( - controller: bioTextController, - minLines: 8, - maxLines: 8, - keyboardType: TextInputType.multiline, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: l10n.profileBio, + ThunderSettingsTile( + leading: Icon(Icons.person_rounded), + title: l10n.displayName, + subtitle: person?.displayName?.isNotEmpty == true ? person?.displayName : l10n.noDisplayNameSet, + trailing: const Padding(padding: EdgeInsets.all(20.0)), + onTap: () { + displayNameTextController.text = person?.displayName ?? ""; + showThunderDialog( + context: context, + title: l10n.displayName, + contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( + controller: displayNameTextController, + decoration: InputDecoration(hintText: l10n.displayName), ), - ), - primaryButtonText: l10n.save, - onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(bio: bioTextController.text)); - Navigator.of(dialogContext).pop(); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - ); - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountProfileBio, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.email_rounded, - description: l10n.email, - subtitle: localUser?.email?.isNotEmpty == true ? localUser?.email : l10n.noEmailSet, - widget: const Padding(padding: EdgeInsets.all(20.0)), - onTap: () { - emailTextController.text = localUser?.email ?? ""; - showThunderDialog( - context: context, - title: l10n.email, - contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( - controller: emailTextController, - decoration: InputDecoration(hintText: l10n.email), - keyboardType: TextInputType.emailAddress, - ), - primaryButtonText: l10n.save, - onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(email: emailTextController.text)); - Navigator.of(dialogContext).pop(); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - ); - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountEmail, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.person_rounded, - description: l10n.matrixUser, - subtitle: person?.matrixUserId?.isNotEmpty == true ? person?.matrixUserId : l10n.noMatrixUserSet, - widget: const Padding(padding: EdgeInsets.all(20.0)), - onTap: () { - matrixUserTextController.text = person?.matrixUserId ?? ""; - showThunderDialog( - context: context, - title: l10n.matrixUser, - contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( - controller: matrixUserTextController, - decoration: const InputDecoration(hintText: "@user:instance"), - ), - primaryButtonText: l10n.save, - onPrimaryButtonPressed: (dialogContext, _) { - context.read().add(UpdateUserSettingsEvent(matrixUserId: matrixUserTextController.text)); - Navigator.of(dialogContext).pop(); - }, - secondaryButtonText: l10n.cancel, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - ); - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountMatrixUser, - highlightedSetting: settingToHighlight, - ), + primaryButtonText: l10n.save, + onPrimaryButtonPressed: (dialogContext, _) { + context.read().add(UpdateUserSettingsEvent(displayName: displayNameTextController.text)); + Navigator.of(dialogContext).pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountDisplayName), + highlighted: settingToHighlight == LocalSettings.accountDisplayName), + ThunderSettingsTile( + leading: Icon(Icons.note_rounded), + title: l10n.profileBio, + subtitle: person?.bio?.isNotEmpty == true ? parse(markdownToHtml(person?.bio ?? "")).documentElement?.text.trim() : l10n.noProfileBioSet, + subtitleMaxLines: 1, + trailing: const Padding(padding: EdgeInsets.all(20.0)), + onTap: () { + bioTextController.text = person?.bio ?? ""; + showThunderDialog( + context: context, + title: l10n.profileBio, + contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( + controller: bioTextController, + minLines: 8, + maxLines: 8, + keyboardType: TextInputType.multiline, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: l10n.profileBio, + ), + ), + primaryButtonText: l10n.save, + onPrimaryButtonPressed: (dialogContext, _) { + context.read().add(UpdateUserSettingsEvent(bio: bioTextController.text)); + Navigator.of(dialogContext).pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountProfileBio), + highlighted: settingToHighlight == LocalSettings.accountProfileBio), + ThunderSettingsTile( + leading: Icon(Icons.email_rounded), + title: l10n.email, + subtitle: localUser?.email?.isNotEmpty == true ? localUser?.email : l10n.noEmailSet, + trailing: const Padding(padding: EdgeInsets.all(20.0)), + onTap: () { + emailTextController.text = localUser?.email ?? ""; + showThunderDialog( + context: context, + title: l10n.email, + contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( + controller: emailTextController, + decoration: InputDecoration(hintText: l10n.email), + keyboardType: TextInputType.emailAddress, + ), + primaryButtonText: l10n.save, + onPrimaryButtonPressed: (dialogContext, _) { + context.read().add(UpdateUserSettingsEvent(email: emailTextController.text)); + Navigator.of(dialogContext).pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountEmail), + highlighted: settingToHighlight == LocalSettings.accountEmail), + ThunderSettingsTile( + leading: Icon(Icons.person_rounded), + title: l10n.matrixUser, + subtitle: person?.matrixUserId?.isNotEmpty == true ? person?.matrixUserId : l10n.noMatrixUserSet, + trailing: const Padding(padding: EdgeInsets.all(20.0)), + onTap: () { + matrixUserTextController.text = person?.matrixUserId ?? ""; + showThunderDialog( + context: context, + title: l10n.matrixUser, + contentWidgetBuilder: (setPrimaryButtonEnabled) => TextField( + controller: matrixUserTextController, + decoration: const InputDecoration(hintText: "@user:instance"), + ), + primaryButtonText: l10n.save, + onPrimaryButtonPressed: (dialogContext, _) { + context.read().add(UpdateUserSettingsEvent(matrixUserId: matrixUserTextController.text)); + Navigator.of(dialogContext).pop(); + }, + secondaryButtonText: l10n.cancel, + onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), + ); + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountMatrixUser), + highlighted: settingToHighlight == LocalSettings.accountMatrixUser), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.feedSettings, style: theme.textTheme.titleMedium), @@ -305,127 +301,118 @@ class _UserSettingsPageState extends State { ), ), ), - ListOption( - description: l10n.defaultFeedType, - value: ListPickerItem(label: localUser?.defaultListingType?.value ?? "", icon: Icons.feed, payload: localUser?.defaultListingType), - options: [ - ListPickerItem(icon: Icons.view_list_rounded, label: FeedListType.subscribed.value, payload: FeedListType.subscribed), - ListPickerItem(icon: Icons.home_rounded, label: FeedListType.all.value, payload: FeedListType.all), - ListPickerItem(icon: Icons.grid_view_rounded, label: FeedListType.local.value, payload: FeedListType.local), - ], - icon: Icons.filter_alt_rounded, - onChanged: (value) async => context.read().add(UpdateUserSettingsEvent(defaultFeedListType: value.payload)), - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountDefaultFeedType, - highlightedSetting: settingToHighlight, - ), - ListOption( - description: l10n.defaultFeedSortType, - value: ListPickerItem( - label: localUser?.defaultSortType?.name ?? "", - icon: Icons.local_fire_department_rounded, - payload: localUser?.defaultSortType, - ), - options: [...getDefaultPostSortTypeItems(account: account), ...getTopPostSortTypeItems(account: account)], - icon: Icons.sort_rounded, - onChanged: (_) async {}, - isBottomModalScrollControlled: true, - customListPicker: SortPicker( - account: account, - title: l10n.defaultFeedSortType, - onSelect: (value) async { - context.read().add(UpdateUserSettingsEvent(defaultPostSortType: value.payload)); - }, - previouslySelected: localUser?.defaultSortType, - ), - valueDisplay: Row( - children: [ - Icon(allPostSortTypeItems.firstWhere((item) => item.payload == localUser?.defaultSortType).icon, size: 13), - const SizedBox(width: 4), - Text( - allPostSortTypeItems.firstWhere((item) => item.payload == localUser?.defaultSortType).label, - style: theme.textTheme.titleSmall, - ), + ThunderListOption( + title: l10n.defaultFeedType, + value: ListPickerItem(label: localUser?.defaultListingType?.value ?? "", icon: Icons.feed, payload: localUser?.defaultListingType), + options: [ + ListPickerItem(icon: Icons.view_list_rounded, label: FeedListType.subscribed.value, payload: FeedListType.subscribed), + ListPickerItem(icon: Icons.home_rounded, label: FeedListType.all.value, payload: FeedListType.all), + ListPickerItem(icon: Icons.grid_view_rounded, label: FeedListType.local.value, payload: FeedListType.local), ], - ), - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountDefaultFeedSortType, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showNsfwContent, - value: localUser?.showNsfw, - iconEnabled: Icons.no_adult_content, - iconDisabled: Icons.no_adult_content, - onToggle: (bool value) => context.read().add(UpdateUserSettingsEvent(showNsfw: value)), - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountShowNsfwContent, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showScores, - value: localUser?.showScores, - iconEnabled: Icons.onetwothree_rounded, - iconDisabled: Icons.onetwothree_rounded, - onToggle: (bool value) => {context.read().add(UpdateUserSettingsEvent(showScores: value))}, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountShowScores, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showReadPosts, - value: localUser?.showReadPosts, - iconEnabled: Icons.fact_check_rounded, - iconDisabled: Icons.fact_check_outlined, - onToggle: (bool value) => {context.read().add(UpdateUserSettingsEvent(showReadPosts: value))}, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountShowReadPosts, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.bot, - value: person?.botAccount, - iconEnabled: Thunder.robot, - iconDisabled: Thunder.robot, - iconSpacing: 14.0, - onToggle: (bool value) => {context.read().add(UpdateUserSettingsEvent(botAccount: value))}, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountIsBot, - highlightedSetting: settingToHighlight, - ), - ToggleOption( - description: l10n.showBotAccounts, - value: localUser?.showBotAccounts, - iconEnabled: Thunder.robot, - iconDisabled: Thunder.robot, - iconSpacing: 14.0, - onToggle: (bool value) => {context.read().add(UpdateUserSettingsEvent(showBotAccounts: value))}, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountShowBotAccounts, - highlightedSetting: settingToHighlight, - ), + leading: Icon(Icons.filter_alt_rounded), + onChanged: (value) async => context.read().add(UpdateUserSettingsEvent(defaultFeedListType: value.payload)), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountDefaultFeedType), + highlighted: settingToHighlight == LocalSettings.accountDefaultFeedType), + ThunderListOption( + title: l10n.defaultFeedSortType, + value: ListPickerItem( + label: localUser?.defaultSortType?.name ?? "", + icon: Icons.local_fire_department_rounded, + payload: localUser?.defaultSortType, + ), + options: [...getDefaultPostSortTypeItems(account: account), ...getTopPostSortTypeItems(account: account)], + leading: Icon(Icons.sort_rounded), + onChanged: (_) async {}, + isBottomModalScrollControlled: true, + customListPicker: SortPicker( + account: account, + title: l10n.defaultFeedSortType, + onSelect: (value) async { + context.read().add(UpdateUserSettingsEvent(defaultPostSortType: value.payload)); + }, + previouslySelected: localUser?.defaultSortType, + ), + valueDisplay: Row( + children: [ + Icon(allPostSortTypeItems.firstWhere((item) => item.payload == localUser?.defaultSortType).icon, size: 13), + const SizedBox(width: 4), + Text( + allPostSortTypeItems.firstWhere((item) => item.payload == localUser?.defaultSortType).label, + style: theme.textTheme.titleSmall, + ), + ], + ), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountDefaultFeedSortType), + highlighted: settingToHighlight == LocalSettings.accountDefaultFeedSortType), + ThunderToggleOption( + title: l10n.showNsfwContent, + value: localUser?.showNsfw, + iconEnabled: Icons.no_adult_content, + iconDisabled: Icons.no_adult_content, + onChanged: (bool value) => context.read().add(UpdateUserSettingsEvent(showNsfw: value)), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowNsfwContent), + highlighted: settingToHighlight == LocalSettings.accountShowNsfwContent), + ThunderToggleOption( + title: l10n.showScores, + value: localUser?.showScores, + iconEnabled: Icons.onetwothree_rounded, + iconDisabled: Icons.onetwothree_rounded, + onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(showScores: value))}, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowScores), + highlighted: settingToHighlight == LocalSettings.accountShowScores), + ThunderToggleOption( + title: l10n.showReadPosts, + value: localUser?.showReadPosts, + iconEnabled: Icons.fact_check_rounded, + iconDisabled: Icons.fact_check_outlined, + onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(showReadPosts: value))}, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowReadPosts), + highlighted: settingToHighlight == LocalSettings.accountShowReadPosts), + ThunderToggleOption( + title: l10n.bot, + value: person?.botAccount, + iconEnabled: Thunder.robot, + iconDisabled: Thunder.robot, + iconSpacing: 14.0, + onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(botAccount: value))}, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountIsBot), + highlighted: settingToHighlight == LocalSettings.accountIsBot), + ThunderToggleOption( + title: l10n.showBotAccounts, + value: localUser?.showBotAccounts, + iconEnabled: Thunder.robot, + iconDisabled: Thunder.robot, + iconSpacing: 14.0, + onChanged: (bool value) => {context.read().add(UpdateUserSettingsEvent(showBotAccounts: value))}, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountShowBotAccounts), + highlighted: settingToHighlight == LocalSettings.accountShowBotAccounts), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.contentManagement, style: theme.textTheme.titleMedium), ), - SettingsListTile( - icon: Icons.language_rounded, - description: l10n.discussionLanguages, - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), - onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccountLanguages), - highlightKey: settingToHighlightKey, - setting: LocalSettings.discussionLanguages, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.block_rounded, - description: l10n.blockSettingLabel, - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), - onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccountBlocks), - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountBlocks, - highlightedSetting: settingToHighlight, - ), + ThunderSettingsTile( + leading: Icon(Icons.language_rounded), + title: l10n.discussionLanguages, + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccountLanguages), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.discussionLanguages), + highlighted: settingToHighlight == LocalSettings.discussionLanguages), + ThunderSettingsTile( + leading: Icon(Icons.block_rounded), + title: l10n.blockSettingLabel, + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccountBlocks), + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountBlocks), + highlighted: settingToHighlight == LocalSettings.accountBlocks), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Column( @@ -436,55 +423,54 @@ class _UserSettingsPageState extends State { ], ), ), - SettingsListTile( - icon: Icons.file_download_rounded, - description: l10n.exportLemmyAccountSettingsDescription, - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), - onTap: () async { - dynamic exportSettings; - try { - final account = context.read().state.account; - exportSettings = await AccountRepositoryImpl(account: account).exportSettings(); - } catch (e) { - // Catch rate-limit errors - showSnackbar(getExceptionErrorMessage(e)); - return; - } - - try { - final String initialFilePath = (await getApplicationDocumentsDirectory()).path; - // Use the same naming convention as the web UI - String initialFileName = 'lemmy_user_settings_${DateTime.now().toUtc().toIso8601String().replaceAll(":", "").replaceAll("-", "")}.json'; - final filePath = '$initialFilePath/$initialFileName'; - - final File file = File(filePath); - await file.writeAsString(jsonEncode(exportSettings)); - - final String? savedFilePath = await FlutterFileDialog.saveFile( - params: SaveFileDialogParams( - mimeTypesFilter: ['application/json'], - sourceFilePath: filePath, - fileName: initialFileName, - ), - ); + ThunderSettingsTile( + leading: Icon(Icons.file_download_rounded), + title: l10n.exportLemmyAccountSettingsDescription, + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + onTap: () async { + dynamic exportSettings; + try { + final account = context.read().state.account; + exportSettings = await AccountRepositoryImpl(account: account).exportSettings(); + } catch (e) { + // Catch rate-limit errors + showSnackbar(getExceptionErrorMessage(e)); + return; + } - if (savedFilePath?.isNotEmpty == true) { - showSnackbar(l10n.accountSettingsExportedSuccessfully(savedFilePath!)); - } else { - showSnackbar(l10n.errorSavingAccountSettings); + try { + final String initialFilePath = (await getApplicationDocumentsDirectory()).path; + // Use the same naming convention as the web UI + String initialFileName = 'lemmy_user_settings_${DateTime.now().toUtc().toIso8601String().replaceAll(":", "").replaceAll("-", "")}.json'; + final filePath = '$initialFilePath/$initialFileName'; + + final File file = File(filePath); + await file.writeAsString(jsonEncode(exportSettings)); + + final String? savedFilePath = await FlutterFileDialog.saveFile( + params: SaveFileDialogParams( + mimeTypesFilter: ['application/json'], + sourceFilePath: filePath, + fileName: initialFileName, + ), + ); + + if (savedFilePath?.isNotEmpty == true) { + showSnackbar(l10n.accountSettingsExportedSuccessfully(savedFilePath!)); + } else { + showSnackbar(l10n.errorSavingAccountSettings); + } + } catch (e) { + showSnackbar('${l10n.errorSavingAccountSettings} $e'); } - } catch (e) { - showSnackbar('${l10n.errorSavingAccountSettings} $e'); - } - }, - highlightKey: settingToHighlightKey, - setting: LocalSettings.accountExportSettings, - highlightedSetting: settingToHighlight, - ), - SettingsListTile( - icon: Icons.file_upload_rounded, - description: l10n.importLemmyAccountSettingsDescription, - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + }, + highlightKey: settingToHighlightKey, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountExportSettings), + highlighted: settingToHighlight == LocalSettings.accountExportSettings), + ThunderSettingsTile( + leading: Icon(Icons.file_upload_rounded), + title: l10n.importLemmyAccountSettingsDescription, + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), onTap: () async { String importSettings; @@ -531,17 +517,17 @@ class _UserSettingsPageState extends State { } }, highlightKey: settingToHighlightKey, - setting: LocalSettings.accountImportSettings, - highlightedSetting: settingToHighlight, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountImportSettings), + highlighted: settingToHighlight == LocalSettings.accountImportSettings, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Text(l10n.dangerZone, style: theme.textTheme.titleMedium), ), - SettingsListTile( - icon: Icons.password, - description: l10n.changePassword, - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + ThunderSettingsTile( + leading: Icon(Icons.password), + title: l10n.changePassword, + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), onTap: () async { showThunderDialog( context: context, @@ -561,13 +547,13 @@ class _UserSettingsPageState extends State { ); }, highlightKey: settingToHighlightKey, - setting: LocalSettings.accountChangePassword, - highlightedSetting: settingToHighlight, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountChangePassword), + highlighted: settingToHighlight == LocalSettings.accountChangePassword, ), - SettingsListTile( - icon: Icons.delete_forever_rounded, - description: l10n.deleteAccount, - widget: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), + ThunderSettingsTile( + leading: Icon(Icons.delete_forever_rounded), + title: l10n.deleteAccount, + trailing: const SizedBox(height: 42.0, child: Icon(Icons.chevron_right_rounded)), onTap: () async { showThunderDialog( context: context, @@ -587,20 +573,20 @@ class _UserSettingsPageState extends State { ); }, highlightKey: settingToHighlightKey, - setting: LocalSettings.accountDeleteAccount, - highlightedSetting: settingToHighlight, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountDeleteAccount), + highlighted: settingToHighlight == LocalSettings.accountDeleteAccount, ), - SettingsListTile( - icon: Icons.hide_image_rounded, - description: l10n.manageMedia, - widget: const SizedBox( + ThunderSettingsTile( + leading: Icon(Icons.hide_image_rounded), + title: l10n.manageMedia, + trailing: const SizedBox( height: 42.0, child: Icon(Icons.chevron_right_rounded), ), onTap: () => navigateToSettingPage(context, LocalSettings.settingsPageAccountMedia), highlightKey: settingToHighlightKey, - setting: LocalSettings.accountManageMedia, - highlightedSetting: settingToHighlight, + onLongPress: () => shareLocalSetting(context, LocalSettings.accountManageMedia), + highlighted: settingToHighlight == LocalSettings.accountManageMedia, ), const SizedBox(height: 100.0), ], diff --git a/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart b/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart index 329a6d929..9ec47b704 100644 --- a/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart +++ b/lib/src/features/user/presentation/widgets/user_action_bottom_sheet.dart @@ -7,7 +7,7 @@ import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/features/post/post.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; import 'package:thunder/src/shared/widgets/chips/user_chip.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; diff --git a/lib/src/features/user/presentation/widgets/user_header/user_header.dart b/lib/src/features/user/presentation/widgets/user_header/user_header.dart index f3a4b8cea..7b280d30f 100644 --- a/lib/src/features/user/presentation/widgets/user_header/user_header.dart +++ b/lib/src/features/user/presentation/widgets/user_header/user_header.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/feed.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; -import 'package:thunder/src/shared/icon_text.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show ImagePreview; +import 'package:thunder/src/shared/content/widgets/media/image_preview.dart'; +import 'package:thunder/packages/ui/ui.dart' show ThunderIconLabel; /// A widget that displays a user's header information and related actions. /// @@ -130,10 +130,9 @@ class _UserInfo extends StatelessWidget { maxLines: 1, ), UserFullNameWidget( - context, - user.name, - user.displayName, - fetchInstanceNameFromUrl(user.actorId), + name: user.name, + displayName: user.displayName, + instance: fetchInstanceNameFromUrl(user.actorId), autoSize: true, useDisplayName: false, // Override because we're showing display name above ), @@ -158,13 +157,13 @@ class _UserStats extends StatelessWidget { return Row( spacing: 10.0, children: [ - IconText( + ThunderIconLabel( icon: Icon(Icons.wysiwyg_rounded, size: iconSize), - text: formatNumberToK(user.posts ?? 0), + label: formatNumberToK(user.posts ?? 0), ), - IconText( + ThunderIconLabel( icon: Icon(Icons.chat_rounded, size: iconSize), - text: formatNumberToK(user.comments ?? 0), + label: formatNumberToK(user.comments ?? 0), ), ], ); diff --git a/lib/src/features/user/presentation/widgets/user_indicator.dart b/lib/src/features/user/presentation/widgets/user_indicator.dart index 5f68c494a..2be610ea8 100644 --- a/lib/src/features/user/presentation/widgets/user_indicator.dart +++ b/lib/src/features/user/presentation/widgets/user_indicator.dart @@ -4,8 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/account.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; @@ -106,10 +106,9 @@ class _UserIndicatorState extends State { children: [ Text(user!.displayNameOrName), UserFullNameWidget( - context, - user!.name, - user!.displayName, - fetchInstanceNameFromUrl(user!.actorId) ?? '-', + name: user!.name, + displayName: user!.displayName, + instance: fetchInstanceNameFromUrl(user!.actorId) ?? '-', // Override because we're showing display name above useDisplayName: false, ), diff --git a/lib/src/features/user/presentation/widgets/user_information.dart b/lib/src/features/user/presentation/widgets/user_information.dart index 9c7472a43..0fb6834bc 100644 --- a/lib/src/features/user/presentation/widgets/user_information.dart +++ b/lib/src/features/user/presentation/widgets/user_information.dart @@ -5,9 +5,9 @@ import 'package:intl/intl.dart'; import 'package:thunder/src/features/community/community.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/feed/feed.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; @@ -190,10 +190,9 @@ class UserModeratorList extends StatelessWidget { ), ), CommunityFullNameWidget( - context, - community.name, - community.title, - fetchInstanceNameFromUrl(community.actorId), + name: community.name, + displayName: community.title, + instance: fetchInstanceNameFromUrl(community.actorId), textStyle: const TextStyle(fontSize: 13.0), transformColor: (color) => color?.withValues(alpha: 0.6), useDisplayName: false, // Override because we're showing display name above diff --git a/lib/src/features/user/presentation/widgets/user_label_chip.dart b/lib/src/features/user/presentation/widgets/user_label_chip.dart index 5caa159e9..da5d839f4 100644 --- a/lib/src/features/user/presentation/widgets/user_label_chip.dart +++ b/lib/src/features/user/presentation/widgets/user_label_chip.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/features/user/user.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/shared/theme/color_utils.dart'; @@ -44,7 +44,7 @@ class UserLabelChip extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 5.0), child: ScalableText( label, - fontScale: metadataFontSizeScale, + textScaleFactor: metadataFontSizeScale.textScaleFactor, ), ), ); diff --git a/lib/src/features/user/presentation/widgets/user_list_entry.dart b/lib/src/features/user/presentation/widgets/user_list_entry.dart index a2ace5658..e0328953b 100644 --- a/lib/src/features/user/presentation/widgets/user_list_entry.dart +++ b/lib/src/features/user/presentation/widgets/user_list_entry.dart @@ -5,8 +5,8 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/feed/feed.dart'; import 'package:thunder/src/features/search/search.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/user/user.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; @@ -39,10 +39,9 @@ class UserListEntry extends StatelessWidget { children: [ Flexible( child: UserFullNameWidget( - context, - user.name, - user.displayName, - fetchInstanceNameFromUrl(user.actorId), + name: user.name, + displayName: user.displayName, + instance: fetchInstanceNameFromUrl(user.actorId), // Override because we're showing display name above useDisplayName: false, ), diff --git a/lib/src/foundation/primitives/models/media.dart b/lib/src/foundation/primitives/models/media.dart index 2fefb4ddf..cc3315f36 100644 --- a/lib/src/foundation/primitives/models/media.dart +++ b/lib/src/foundation/primitives/models/media.dart @@ -71,6 +71,8 @@ bool _isImageUrl(String url) { '@jpeg', ]; + if (url.contains('/image_proxy')) return true; + final uri = Uri.tryParse(url); if (uri == null) return false; diff --git a/lib/packages/ui/src/utils/markdown/markdown_utils.dart b/lib/src/shared/content/utils/markdown/markdown_utils.dart similarity index 100% rename from lib/packages/ui/src/utils/markdown/markdown_utils.dart rename to lib/src/shared/content/utils/markdown/markdown_utils.dart diff --git a/lib/packages/ui/src/utils/media/media_utils.dart b/lib/src/shared/content/utils/media/media_utils.dart similarity index 84% rename from lib/packages/ui/src/utils/media/media_utils.dart rename to lib/src/shared/content/utils/media/media_utils.dart index 03be10e2f..44a3e7536 100644 --- a/lib/packages/ui/src/utils/media/media_utils.dart +++ b/lib/src/shared/content/utils/media/media_utils.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:youtube_player_flutter/youtube_player_flutter.dart'; import 'package:flutter_avif/flutter_avif.dart'; @@ -15,7 +17,9 @@ import 'package:image/image.dart' as img; import 'package:image_picker/image_picker.dart'; import 'package:image_dimension_parser/image_dimension_parser.dart'; -import 'package:thunder/packages/ui/src/widgets/media/image_viewer.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; +import 'package:thunder/src/shared/content/widgets/media/image_viewer.dart'; final Map _imageDimensionsCache = {}; @@ -84,15 +88,15 @@ Future isImageUrlSvg(String imageUrl) async { } Future isImageUriSvg(Uri? imageUri) async { + if (imageUri == null) return false; + try { - final http.Response response = await http.get( - imageUri ?? Uri(), - headers: { - 'method': 'HEAD', - 'Range': 'bytes=0-0', - }, - ); - return response.headers['content-type']?.toLowerCase().contains('svg') == true; + final response = await http.head(imageUri, headers: {'Range': 'bytes=0-0'}); + final contentType = response.headers['content-type']?.toLowerCase(); + if (contentType != null) return contentType.contains('svg'); + + final fallbackResponse = await http.get(imageUri, headers: {'Range': 'bytes=0-0'}); + return fallbackResponse.headers['content-type']?.toLowerCase().contains('svg') == true; } catch (e) { return false; } @@ -161,7 +165,14 @@ Future retrieveImageDimensions({String? imageUrl, Uint8List? imageBytes}) if (size != null) return size; } - Uint8List? data = imageBytes; + final data = imageBytes; + + if (data != null) { + final decoded = img.decodeImage(data); + if (decoded != null) { + size = Size(decoded.width.toDouble(), decoded.height.toDouble()); + } + } if (data == null && imageUrl != null) { try { @@ -216,15 +227,19 @@ Future> selectImagesToUpload({bool allowMultiple = false}) async { final ImagePicker picker = ImagePicker(); if (allowMultiple) { - List? files = await picker.pickMultiImage(); + final files = await picker.pickMultiImage(); return files.map((file) => file.path).toList(); } - XFile? file = await picker.pickImage(source: ImageSource.gallery); - return [file!.path]; + final file = await picker.pickImage(source: ImageSource.gallery); + if (file == null) return []; + + return [file.path]; } -void showImageViewer(BuildContext context, {String? url, Uint8List? bytes, int? postId, void Function()? navigateToPost, String? altText, bool clearMemoryCacheWhenDispose = false}) { +void showImageViewer(BuildContext context, {String? url, Uint8List? bytes, int? postId, void Function()? navigateToPost, String? altText, bool? clearMemoryCacheWhenDispose}) { + final resolvedClearMemoryCacheWhenDispose = clearMemoryCacheWhenDispose ?? context.read().state.imageCachingMode == ImageCachingMode.relaxed; + Navigator.of(context).push( PageRouteBuilder( opaque: false, @@ -237,7 +252,7 @@ void showImageViewer(BuildContext context, {String? url, Uint8List? bytes, int? postId: postId, navigateToPost: navigateToPost, altText: altText, - clearMemoryCacheWhenDispose: clearMemoryCacheWhenDispose, + clearMemoryCacheWhenDispose: resolvedClearMemoryCacheWhenDispose, ); }, transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { diff --git a/lib/packages/ui/src/widgets/markdown/common_markdown_body.dart b/lib/src/shared/content/widgets/markdown/common_markdown_body.dart similarity index 69% rename from lib/packages/ui/src/widgets/markdown/common_markdown_body.dart rename to lib/src/shared/content/widgets/markdown/common_markdown_body.dart index 5952e1d0f..f7100c4ab 100644 --- a/lib/packages/ui/src/widgets/markdown/common_markdown_body.dart +++ b/lib/src/shared/content/widgets/markdown/common_markdown_body.dart @@ -1,22 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/features/settings/api.dart'; +import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; +import 'package:thunder/src/shared/links/widgets/link_bottom_sheet.dart'; + import 'package:jovial_svg/jovial_svg.dart'; import 'package:markdown/markdown.dart' as md; import 'package:flutter_markdown/flutter_markdown.dart'; -import 'package:thunder/packages/ui/src/utils/media/media_utils.dart'; -import 'package:thunder/packages/ui/src/models/content/content_action_handlers.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media_type.dart'; -import 'package:thunder/packages/ui/src/models/content/content_view_mode.dart'; -import 'package:thunder/packages/ui/src/widgets/markdown/extended_markdown.dart'; -import 'package:thunder/packages/ui/src/widgets/markdown/markdown_lemmy_link.dart'; -import 'package:thunder/packages/ui/src/widgets/markdown/markdown_spoiler.dart'; -import 'package:thunder/packages/ui/src/widgets/markdown/markdown_subsuperscript.dart'; -import 'package:thunder/packages/ui/src/utils/markdown/markdown_utils.dart'; -import 'package:thunder/packages/ui/src/widgets/media/link_information.dart'; -import 'package:thunder/packages/ui/src/widgets/media/media_view.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/extended_markdown.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/markdown_lemmy_link.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/markdown_spoiler.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/markdown_subsuperscript.dart'; +import 'package:thunder/src/shared/content/utils/markdown/markdown_utils.dart'; +import 'package:thunder/src/shared/content/widgets/media/link_information.dart'; +import 'package:thunder/src/shared/content/widgets/media/media_view.dart'; /// A widget that displays markdown content. class CommonMarkdownBody extends StatefulWidget { @@ -35,21 +38,6 @@ class CommonMarkdownBody extends StatefulWidget { /// The maximum width of the image. final double? imageMaxWidth; - /// Optional action handlers that decouple media and navigation behavior. - final ContentActionHandlers handlers; - - /// Text scale factor used for comment markdown. - final double commentTextScaleFactor; - - /// Text scale factor used for non-comment markdown. - final double contentTextScaleFactor; - - /// Localized retry tooltip for image fallbacks. - final String retryTooltip; - - /// Localized NSFW warning label. - final String nsfwWarningLabel; - const CommonMarkdownBody({ super.key, required this.body, @@ -57,11 +45,6 @@ class CommonMarkdownBody extends StatefulWidget { this.nsfw = false, this.isComment, this.imageMaxWidth, - this.handlers = const ContentActionHandlers(), - this.commentTextScaleFactor = 1.0, - this.contentTextScaleFactor = 1.0, - this.retryTooltip = 'Retry', - this.nsfwWarningLabel = 'NSFW', }); @override @@ -114,7 +97,9 @@ class _CommonMarkdownBodyState extends State { double _getTextScaleFactor() { final baseScale = MediaQuery.textScalerOf(context).scale(1.0); - final fontScale = widget.isComment == true ? widget.commentTextScaleFactor : widget.contentTextScaleFactor; + final resolvedCommentTextScaleFactor = context.select((cubit) => cubit.state.commentFontSizeScale).textScaleFactor; + final resolvedContentTextScaleFactor = context.select((cubit) => cubit.state.contentFontSizeScale).textScaleFactor; + final fontScale = widget.isComment == true ? resolvedCommentTextScaleFactor : resolvedContentTextScaleFactor; return baseScale * fontScale; } @@ -144,17 +129,12 @@ class _CommonMarkdownBodyState extends State { nsfw: widget.nsfw, isComment: widget.isComment, imageMaxWidth: widget.imageMaxWidth, - handlers: widget.handlers, - retryTooltip: widget.retryTooltip, - nsfwWarningLabel: widget.nsfwWarningLabel, ), onTapLink: (text, url, title) { - if (url != null) { - widget.handlers.onOpenLink?.call(context, url); - } + if (url != null) handleLink(context, url: url); }, onLongPressLink: (text, url, title) { - widget.handlers.onLongPressLink?.call(context, text, url); + if (url != null) handleLinkLongPress(context, text, url); }, styleSheet: styleSheet.copyWith(textScaleFactor: _getTextScaleFactor()), ), @@ -180,15 +160,6 @@ class MarkdownImageWidget extends StatefulWidget { /// The maximum width of the image. final double? imageMaxWidth; - /// Optional action handlers that decouple media and navigation behavior. - final ContentActionHandlers handlers; - - /// Localized retry tooltip for image fallback. - final String retryTooltip; - - /// Localized NSFW warning label. - final String nsfwWarningLabel; - const MarkdownImageWidget({ super.key, required this.uri, @@ -196,9 +167,6 @@ class MarkdownImageWidget extends StatefulWidget { required this.nsfw, this.isComment, this.imageMaxWidth, - this.handlers = const ContentActionHandlers(), - this.retryTooltip = 'Retry', - this.nsfwWarningLabel = 'NSFW', }); /// Holds a cache of previously retrieved SVG results. @@ -213,7 +181,7 @@ class _MarkdownImageWidgetState extends State { String? uri; /// The media type of the URL. - ContentMediaType? mediaType; + MediaType? mediaType; /// The dimensions of the image. Size? dimensions; @@ -225,10 +193,10 @@ class _MarkdownImageWidgetState extends State { uri = Uri.decodeFull(widget.uri.toString()); if (isImageUrl(uri!)) { - mediaType = ContentMediaType.image; + mediaType = MediaType.image; _getImageDimensions(); } else if (isVideoUrl(uri!)) { - mediaType = ContentMediaType.video; + mediaType = MediaType.video; } else { _checkSVG(); } @@ -253,11 +221,15 @@ class _MarkdownImageWidgetState extends State { Future _checkSVG() async { try { - if (MarkdownImageWidget._svgCache.containsKey(uri)) return; + if (MarkdownImageWidget._svgCache.containsKey(uri)) { + if (mounted) setState(() {}); + return; + } + final result = await isImageUriSvg(widget.uri); MarkdownImageWidget._svgCache[uri!] = result; - setState(() {}); + if (mounted) setState(() {}); } catch (e) { debugPrint('Error checking SVG: $uri - $e'); return; @@ -266,24 +238,24 @@ class _MarkdownImageWidgetState extends State { @override Widget build(BuildContext context) { - if (mediaType == ContentMediaType.video) { + if (mediaType == MediaType.video) { return LinkInformation( - viewMode: ContentViewMode.comfortable, + viewMode: ViewMode.comfortable, mediaType: mediaType, url: uri, showEdgeToEdgeImages: false, onTap: () { - if (uri != null) widget.handlers.onOpenLink?.call(context, uri!); + if (uri != null) handleLink(context, url: uri!); }, onLongPress: () { if (uri != null) { - widget.handlers.onLongPressLink?.call(context, uri!, uri); + handleLinkLongPress(context, uri!, uri); } }, ); } - final isSvg = MarkdownImageWidget._svgCache.containsKey(uri); + final isSvg = MarkdownImageWidget._svgCache[uri] == true; return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -297,13 +269,10 @@ class _MarkdownImageWidgetState extends State { imageMaxWidth: widget.imageMaxWidth, ) : MediaView( - viewMode: ContentViewMode.comment, + viewMode: ViewMode.comment, hideNsfwPreviews: widget.nsfw, - handlers: widget.handlers, - retryTooltip: widget.retryTooltip, - nsfwWarningLabel: widget.nsfwWarningLabel, - media: ContentMedia( - mediaType: ContentMediaType.image, + media: Media( + mediaType: MediaType.image, mediaUrl: uri, nsfw: widget.nsfw, width: dimensions?.width, diff --git a/lib/packages/ui/src/widgets/markdown/extended_markdown.dart b/lib/src/shared/content/widgets/markdown/extended_markdown.dart similarity index 100% rename from lib/packages/ui/src/widgets/markdown/extended_markdown.dart rename to lib/src/shared/content/widgets/markdown/extended_markdown.dart diff --git a/lib/packages/ui/src/widgets/markdown/markdown_lemmy_link.dart b/lib/src/shared/content/widgets/markdown/markdown_lemmy_link.dart similarity index 100% rename from lib/packages/ui/src/widgets/markdown/markdown_lemmy_link.dart rename to lib/src/shared/content/widgets/markdown/markdown_lemmy_link.dart diff --git a/lib/packages/ui/src/widgets/markdown/markdown_spoiler.dart b/lib/src/shared/content/widgets/markdown/markdown_spoiler.dart similarity index 98% rename from lib/packages/ui/src/widgets/markdown/markdown_spoiler.dart rename to lib/src/shared/content/widgets/markdown/markdown_spoiler.dart index 1cdea220d..a1176049b 100644 --- a/lib/packages/ui/src/widgets/markdown/markdown_spoiler.dart +++ b/lib/src/shared/content/widgets/markdown/markdown_spoiler.dart @@ -4,7 +4,7 @@ import 'package:expandable/expandable.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:markdown/markdown.dart' as md; -import 'package:thunder/packages/ui/src/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; /// Markdown inline syntax for spoiler tags. class SpoilerInlineSyntax extends md.InlineSyntax { diff --git a/lib/packages/ui/src/widgets/markdown/markdown_subsuperscript.dart b/lib/src/shared/content/widgets/markdown/markdown_subsuperscript.dart similarity index 100% rename from lib/packages/ui/src/widgets/markdown/markdown_subsuperscript.dart rename to lib/src/shared/content/widgets/markdown/markdown_subsuperscript.dart diff --git a/lib/src/features/content/presentation/widgets/media/compact_thumbnail_preview.dart b/lib/src/shared/content/widgets/media/compact_thumbnail_preview.dart similarity index 85% rename from lib/src/features/content/presentation/widgets/media/compact_thumbnail_preview.dart rename to lib/src/shared/content/widgets/media/compact_thumbnail_preview.dart index 9fad9e1c7..75f895ae2 100644 --- a/lib/src/features/content/presentation/widgets/media/compact_thumbnail_preview.dart +++ b/lib/src/shared/content/widgets/media/compact_thumbnail_preview.dart @@ -6,20 +6,12 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/account/api.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/shared/widgets/media/media_type_badge.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_view.dart'; +import 'package:thunder/src/shared/content/widgets/media/media_view.dart'; -/// App adapter for compact media thumbnail previews. class CompactThumbnailPreview extends StatelessWidget { - /// The media to display in the thumbnail. final Media media; - - /// Whether or not to dim the thumbnail. final bool dim; - - /// The post associated with the media. final int? postId; - - /// The callback function to navigate to the post. final void Function()? navigateToPost; const CompactThumbnailPreview({ diff --git a/lib/packages/ui/src/widgets/media/image_preview.dart b/lib/src/shared/content/widgets/media/image_preview.dart similarity index 95% rename from lib/packages/ui/src/widgets/media/image_preview.dart rename to lib/src/shared/content/widgets/media/image_preview.dart index d1d359d0e..e04dfe7e5 100644 --- a/lib/packages/ui/src/widgets/media/image_preview.dart +++ b/lib/src/shared/content/widgets/media/image_preview.dart @@ -2,11 +2,12 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_avif/flutter_avif.dart'; -import 'package:thunder/packages/ui/src/utils/media/media_utils.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media_type.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; /// The loading state of an image preview. enum ImagePreviewState { @@ -38,7 +39,7 @@ class ImagePreview extends StatefulWidget { final BoxFit? fit; /// The media type that the underlying image represents. - final ContentMediaType? mediaType; + final MediaType? mediaType; /// Whether the image has been viewed. This will affect the opacity of the image. final bool? viewed; @@ -172,7 +173,7 @@ class _ImageContent extends StatelessWidget { final bool? blur; /// The media type that the underlying image represents. - final ContentMediaType? mediaType; + final MediaType? mediaType; /// Whether the image can be retried with the original URL. final bool canRetry; @@ -327,7 +328,7 @@ class _BlurredImage extends StatelessWidget { /// Displays the fallback widget when an image fails to load. class ImagePreviewError extends StatelessWidget { /// The media type that the underlying image represents. - final ContentMediaType? mediaType; + final MediaType? mediaType; /// Whether the image should be blurred. final bool blur; @@ -355,15 +356,15 @@ class ImagePreviewError extends StatelessWidget { }); /// Returns the icon to display when the image fails to load. - static IconData _getErrorIcon(ContentMediaType? mediaType) { + static IconData _getErrorIcon(MediaType? mediaType) { switch (mediaType) { - case ContentMediaType.image: + case MediaType.image: return Icons.image_not_supported_outlined; - case ContentMediaType.video: + case MediaType.video: return Icons.video_camera_back_outlined; - case ContentMediaType.link: + case MediaType.link: return Icons.language_rounded; - case ContentMediaType.text: + case MediaType.text: return Icons.text_fields_rounded; default: return Icons.error_outline_rounded; diff --git a/lib/packages/ui/src/widgets/media/image_viewer.dart b/lib/src/shared/content/widgets/media/image_viewer.dart similarity index 99% rename from lib/packages/ui/src/widgets/media/image_viewer.dart rename to lib/src/shared/content/widgets/media/image_viewer.dart index 59c59ad8d..8c6a306d6 100644 --- a/lib/packages/ui/src/widgets/media/image_viewer.dart +++ b/lib/src/shared/content/widgets/media/image_viewer.dart @@ -14,8 +14,8 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/packages/ui/src/widgets/feedback/snackbar.dart'; -import 'package:thunder/packages/ui/src/utils/media/media_utils.dart'; +import 'package:thunder/packages/ui/ui.dart' show showSnackbar; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; class ImageViewer extends StatefulWidget { /// The URL of the image to display diff --git a/lib/packages/ui/src/widgets/media/link_information.dart b/lib/src/shared/content/widgets/media/link_information.dart similarity index 80% rename from lib/packages/ui/src/widgets/media/link_information.dart rename to lib/src/shared/content/widgets/media/link_information.dart index 2e7975e63..d926e0c8e 100644 --- a/lib/packages/ui/src/widgets/media/link_information.dart +++ b/lib/src/shared/content/widgets/media/link_information.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media_type.dart'; -import 'package:thunder/packages/ui/src/models/content/content_view_mode.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; /// A generic widget that displays information about a media/link URL. class LinkInformation extends StatelessWidget { @@ -16,17 +15,17 @@ class LinkInformation extends StatelessWidget { }); final String? url; - final ContentMediaType? mediaType; - final ContentViewMode viewMode; + final MediaType? mediaType; + final ViewMode viewMode; final bool showEdgeToEdgeImages; final VoidCallback? onTap; final VoidCallback? onLongPress; IconData _getIconForMediaType() { return switch (mediaType) { - ContentMediaType.image => Icons.image_outlined, - ContentMediaType.video => Icons.play_arrow_rounded, - ContentMediaType.text => Icons.wysiwyg_rounded, + MediaType.image => Icons.image_outlined, + MediaType.video => Icons.play_arrow_rounded, + MediaType.text => Icons.wysiwyg_rounded, _ => Icons.link_rounded, }; } @@ -59,7 +58,7 @@ class LinkInformation extends StatelessWidget { padding: const EdgeInsets.only(right: 8.0), child: Icon(icon, color: theme.colorScheme.onSecondaryContainer), ), - if (viewMode != ContentViewMode.compact) + if (viewMode != ViewMode.compact) Expanded( child: Text( url ?? '', diff --git a/lib/packages/ui/src/widgets/media/media_view.dart b/lib/src/shared/content/widgets/media/media_view.dart similarity index 63% rename from lib/packages/ui/src/widgets/media/media_view.dart rename to lib/src/shared/content/widgets/media/media_view.dart index de7b4cb36..34b6c78ec 100644 --- a/lib/packages/ui/src/widgets/media/media_view.dart +++ b/lib/src/shared/content/widgets/media/media_view.dart @@ -3,19 +3,26 @@ import 'dart:typed_data'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:thunder/packages/ui/src/widgets/media/image_preview.dart'; -import 'package:thunder/packages/ui/src/widgets/media/image_viewer.dart'; -import 'package:thunder/packages/ui/src/utils/media/media_utils.dart'; -import 'package:thunder/packages/ui/src/models/content/content_action_handlers.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media.dart'; -import 'package:thunder/packages/ui/src/models/content/content_media_type.dart'; -import 'package:thunder/packages/ui/src/models/content/content_view_mode.dart'; -import 'package:thunder/packages/ui/src/widgets/media/link_information.dart'; -import 'package:thunder/packages/ui/src/widgets/media/media_view_text.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/features/post/api.dart'; +import 'package:thunder/src/features/feed/api.dart'; +import 'package:thunder/src/features/settings/api.dart'; +import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; +import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; +import 'package:thunder/src/shared/links/widgets/link_bottom_sheet.dart'; + +import 'package:thunder/src/shared/content/widgets/media/image_preview.dart'; +import 'package:thunder/src/shared/content/widgets/media/image_viewer.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; +import 'package:thunder/src/shared/content/widgets/media/link_information.dart'; +import 'package:thunder/src/shared/content/widgets/media/media_view_text.dart'; class MediaView extends StatefulWidget { /// The media information. - final ContentMedia media; + final Media media; /// The associated post ID for the media. final int? postId; @@ -42,7 +49,7 @@ class MediaView extends StatefulWidget { final bool isUserLoggedIn; /// The view mode of the media. - final ContentViewMode viewMode; + final ViewMode viewMode; /// The function to navigate to the post. final void Function()? navigateToPost; @@ -50,21 +57,6 @@ class MediaView extends StatefulWidget { /// Whether the post has been read. final bool? read; - /// Optional action handlers that decouple media and navigation behavior. - final ContentActionHandlers handlers; - - /// Duration before peek preview starts when long pressing an image. - final int imagePeekDurationMs; - - /// Whether content should be laid out in tablet mode. - final bool tabletMode; - - /// Localized NSFW warning label shown in comfortable view. - final String nsfwWarningLabel; - - /// Localized retry tooltip used by image fallback UI. - final String retryTooltip; - const MediaView({ super.key, required this.media, @@ -76,14 +68,9 @@ class MediaView extends StatefulWidget { this.hideThumbnails = false, this.markPostReadOnMediaView = false, this.isUserLoggedIn = false, - this.viewMode = ContentViewMode.comfortable, + this.viewMode = ViewMode.comfortable, this.navigateToPost, this.read, - this.handlers = const ContentActionHandlers(), - this.imagePeekDurationMs = 300, - this.tabletMode = false, - this.nsfwWarningLabel = 'NSFW', - this.retryTooltip = 'Retry', }); @override @@ -121,43 +108,44 @@ class _MediaViewState extends State with TickerProviderStateMixin { void _markPostAsRead() { if (!widget.isUserLoggedIn || !widget.markPostReadOnMediaView) return; - widget.handlers.onMarkRead?.call(widget.postId); + + try { + final feedBloc = BlocProvider.of(context); + feedBloc.add( + FeedItemActionedEvent( + postAction: PostAction.read, + postId: widget.postId, + actionInput: const ReadPostInput(true), + ), + ); + } catch (e) { + debugPrint('Error marking post as read: $e'); + } } void _openLink(String url) { - widget.handlers.onOpenLink?.call(context, url); + handleLink(context, url: url); } - void _openImage({String? url, Uint8List? bytes}) { - final onOpenImage = widget.handlers.onOpenImage; - if (onOpenImage != null) { - onOpenImage(context, url: url, bytes: bytes); - return; + void _openLinkLongPress(String text, String? url) { + if (url != null) { + handleLinkLongPress(context, text, url); } + } - Navigator.of(context).push( - PageRouteBuilder( - opaque: false, - transitionDuration: const Duration(milliseconds: 100), - reverseTransitionDuration: const Duration(milliseconds: 100), - transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { - return FadeTransition(opacity: animation, child: child); - }, - pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) { - return ImageViewer( - url: url, - bytes: bytes, - postId: widget.postId, - navigateToPost: widget.navigateToPost, - altText: widget.media.altText, - ); - }, - ), + void _openImage({String? url, Uint8List? bytes}) { + showImageViewer( + context, + url: url, + bytes: bytes, + postId: widget.postId, + navigateToPost: widget.navigateToPost, + altText: widget.media.altText, ); } void _openVideo(String url) { - widget.handlers.onOpenVideo?.call(context, url); + handleVideoLink(context, url: url); } /// Overlays the image as an ImageViewer. @@ -167,8 +155,8 @@ class _MediaViewState extends State with TickerProviderStateMixin { } double getMinHeight() { - if (!widget.showFullHeightImages && widget.viewMode != ContentViewMode.comment) { - return ContentViewMode.comfortable.height; + if (!widget.showFullHeightImages && widget.viewMode != ViewMode.comment) { + return ViewMode.comfortable.height; } if (widget.media.height != null) { @@ -178,16 +166,16 @@ class _MediaViewState extends State with TickerProviderStateMixin { return widget.media.height!; } - return ContentViewMode.comfortable.height; + return ViewMode.comfortable.height; } double getMaxHeight() { - if (widget.viewMode != ContentViewMode.comment) { + if (widget.viewMode != ViewMode.comment) { if (widget.allowUnconstrainedImageHeight) { return MediaQuery.of(context).size.height; } if (!widget.showFullHeightImages) { - return ContentViewMode.comfortable.height; + return ViewMode.comfortable.height; } } @@ -198,7 +186,7 @@ class _MediaViewState extends State with TickerProviderStateMixin { return widget.media.height!; } - return ContentViewMode.comfortable.height; + return ViewMode.comfortable.height; } void handleTap() { @@ -207,12 +195,16 @@ class _MediaViewState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { + final imagePeekDurationMs = context.select((cubit) => cubit.state.imagePeekDuration); + final tabletMode = widget.viewMode == ViewMode.comfortable ? context.select((ThunderBloc bloc) => bloc.state.tabletMode) : false; + final l10n = AppLocalizations.of(context)!; + final imageUrlCandidate = widget.media.imageUrl ?? widget.media.mediaUrl ?? widget.media.originalUrl; final isImage = isImageUrl(imageUrlCandidate ?? ''); // If hiding thumbnails is enabled or if the media has no image URL, // display a link preview instead in comfortable mode. - if (widget.viewMode == ContentViewMode.comfortable && (widget.hideThumbnails || !isImage)) { + if (widget.viewMode == ViewMode.comfortable && (widget.hideThumbnails || !isImage)) { return LinkInformation( viewMode: widget.viewMode, url: widget.media.originalUrl, @@ -224,15 +216,13 @@ class _MediaViewState extends State with TickerProviderStateMixin { }, onLongPress: () { final url = widget.media.originalUrl; - if (url != null) { - widget.handlers.onLongPressLink?.call(context, url, url); - } + if (url != null) _openLinkLongPress(url, url); }, showEdgeToEdgeImages: widget.edgeToEdgeImages, ); } - if (widget.viewMode == ContentViewMode.compact && widget.media.mediaType == ContentMediaType.text) { + if (widget.viewMode == ViewMode.compact && widget.media.mediaType == MediaType.text) { return MediaViewText( text: widget.media.altText, read: widget.read, @@ -248,28 +238,25 @@ class _MediaViewState extends State with TickerProviderStateMixin { double? height; switch (widget.viewMode) { - case ContentViewMode.comment: + case ViewMode.comment: width = widget.media.width; - height = widget.media.height ?? ContentViewMode.comment.height; + height = widget.media.height ?? ViewMode.comment.height; break; - case ContentViewMode.compact: + case ViewMode.compact: width = null; - height = ContentViewMode.compact.height; + height = ViewMode.compact.height; break; - case ContentViewMode.comfortable: - width = (widget.tabletMode ? (MediaQuery.of(context).size.width / 2) - 24.0 : MediaQuery.of(context).size.width) - (widget.edgeToEdgeImages ? 0 : 24); + case ViewMode.comfortable: + width = (tabletMode ? (MediaQuery.of(context).size.width / 2) - 24.0 : MediaQuery.of(context).size.width) - (widget.edgeToEdgeImages ? 0 : 24); height = (widget.showFullHeightImages && !widget.allowUnconstrainedImageHeight) ? widget.media.height : null; } - final shouldContainTallFullHeightImage = widget.media.mediaType == ContentMediaType.image && - widget.viewMode == ContentViewMode.comfortable && - widget.showFullHeightImages && - widget.media.height != null && - widget.media.height! > getMaxHeight(); + final shouldContainTallFullHeightImage = + widget.media.mediaType == MediaType.image && widget.viewMode == ViewMode.comfortable && widget.showFullHeightImages && widget.media.height != null && widget.media.height! > getMaxHeight(); Widget? child; - if (widget.media.mediaType == ContentMediaType.link) { + if (widget.media.mediaType == MediaType.link) { child = InkWell( splashColor: theme.colorScheme.primary.withValues(alpha: 0.4), borderRadius: BorderRadius.circular((widget.edgeToEdgeImages ? 0 : 12)), @@ -280,11 +267,9 @@ class _MediaViewState extends State with TickerProviderStateMixin { }, onLongPress: () { final url = widget.media.originalUrl; - if (url != null) { - widget.handlers.onLongPressLink?.call(context, url, url); - } + if (url != null) _openLinkLongPress(url, url); }, - child: widget.viewMode == ContentViewMode.comfortable + child: widget.viewMode == ViewMode.comfortable ? SizedBox( height: 70.0, child: Align( @@ -301,8 +286,8 @@ class _MediaViewState extends State with TickerProviderStateMixin { ); } - if (widget.media.mediaType == ContentMediaType.image && _imagePreviewState == ImagePreviewState.success) { - final imagePeekDuration = Duration(milliseconds: widget.imagePeekDurationMs); + if (widget.media.mediaType == MediaType.image && _imagePreviewState == ImagePreviewState.success) { + final imagePeekDuration = Duration(milliseconds: imagePeekDurationMs); child = InkWell( splashColor: theme.colorScheme.primary.withValues(alpha: 0.4), @@ -346,7 +331,7 @@ class _MediaViewState extends State with TickerProviderStateMixin { ); } - if (widget.media.mediaType == ContentMediaType.video) { + if (widget.media.mediaType == MediaType.video) { child = InkWell( splashColor: theme.colorScheme.primary.withValues(alpha: 0.4), borderRadius: BorderRadius.circular((widget.edgeToEdgeImages ? 0 : 12)), @@ -355,7 +340,7 @@ class _MediaViewState extends State with TickerProviderStateMixin { final url = widget.media.mediaUrl ?? widget.media.originalUrl; if (url != null) _openVideo(url); }, - child: widget.viewMode == ContentViewMode.comfortable + child: widget.viewMode == ViewMode.comfortable ? Column( children: [ const Expanded(child: Icon(Icons.play_arrow_rounded, size: 55)), @@ -387,24 +372,24 @@ class _MediaViewState extends State with TickerProviderStateMixin { ), constraints: BoxConstraints( maxHeight: switch (widget.viewMode) { - ContentViewMode.comment => getMaxHeight(), - ContentViewMode.compact => ContentViewMode.compact.height, - ContentViewMode.comfortable => getMaxHeight(), + ViewMode.comment => getMaxHeight(), + ViewMode.compact => ViewMode.compact.height, + ViewMode.comfortable => getMaxHeight(), }, minHeight: switch (widget.viewMode) { - ContentViewMode.comment => getMinHeight(), - ContentViewMode.compact => ContentViewMode.compact.height, - ContentViewMode.comfortable => getMinHeight(), + ViewMode.comment => getMinHeight(), + ViewMode.compact => ViewMode.compact.height, + ViewMode.comfortable => getMinHeight(), }, maxWidth: switch (widget.viewMode) { - ContentViewMode.comment => MediaQuery.of(context).size.width / 2, - ContentViewMode.compact => ContentViewMode.compact.height, - ContentViewMode.comfortable => widget.edgeToEdgeImages ? double.infinity : MediaQuery.of(context).size.width, + ViewMode.comment => MediaQuery.of(context).size.width / 2, + ViewMode.compact => ViewMode.compact.height, + ViewMode.comfortable => widget.edgeToEdgeImages ? double.infinity : MediaQuery.of(context).size.width, }, minWidth: switch (widget.viewMode) { - ContentViewMode.comment => 0, - ContentViewMode.compact => ContentViewMode.compact.height, - ContentViewMode.comfortable => widget.edgeToEdgeImages ? double.infinity : MediaQuery.of(context).size.width, + ViewMode.comment => 0, + ViewMode.compact => ViewMode.compact.height, + ViewMode.comfortable => widget.edgeToEdgeImages ? double.infinity : MediaQuery.of(context).size.width, }, ), child: Stack( @@ -416,7 +401,7 @@ class _MediaViewState extends State with TickerProviderStateMixin { contentType: widget.media.contentType, width: width, height: height, - fit: widget.viewMode == ContentViewMode.compact + fit: widget.viewMode == ViewMode.compact ? BoxFit.cover : shouldContainTallFullHeightImage ? BoxFit.contain @@ -424,9 +409,9 @@ class _MediaViewState extends State with TickerProviderStateMixin { mediaType: widget.media.mediaType, viewed: widget.read, blur: blurNSFWPreviews, - allowRetry: widget.media.mediaType == ContentMediaType.image, + allowRetry: widget.media.mediaType == MediaType.image, onStateChanged: _onImagePreviewStateChanged, - retryTooltip: widget.retryTooltip, + retryTooltip: l10n.retry, ), if (blurNSFWPreviews) Column( @@ -434,13 +419,13 @@ class _MediaViewState extends State with TickerProviderStateMixin { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - widget.media.mediaType == ContentMediaType.image - ? Icon(Icons.warning_rounded, size: widget.viewMode != ContentViewMode.compact ? 55 : 30) + widget.media.mediaType == MediaType.image + ? Icon(Icons.warning_rounded, size: widget.viewMode != ViewMode.compact ? 55 : 30) : Icon( - widget.viewMode != ContentViewMode.compact ? Icons.play_arrow_rounded : Icons.warning_rounded, - size: widget.viewMode != ContentViewMode.compact ? 55 : 30, + widget.viewMode != ViewMode.compact ? Icons.play_arrow_rounded : Icons.warning_rounded, + size: widget.viewMode != ViewMode.compact ? 55 : 30, ), - if (widget.viewMode == ContentViewMode.comfortable) Text(widget.nsfwWarningLabel, textScaler: const TextScaler.linear(1.5)), + if (widget.viewMode == ViewMode.comfortable) Text(l10n.nsfwWarning, textScaler: const TextScaler.linear(1.5)), ], ), if (child != null) diff --git a/lib/packages/ui/src/widgets/media/media_view_text.dart b/lib/src/shared/content/widgets/media/media_view_text.dart similarity index 100% rename from lib/packages/ui/src/widgets/media/media_view_text.dart rename to lib/src/shared/content/widgets/media/media_view_text.dart diff --git a/lib/src/shared/icon_text.dart b/lib/src/shared/icon_text.dart deleted file mode 100644 index 2d8b92320..000000000 --- a/lib/src/shared/icon_text.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; - -/// Creates a widget that displays an icon followed by text. -/// -/// The [icon] parameter must not be null. -class IconText extends StatelessWidget { - const IconText({ - super.key, - required this.icon, - this.text, - this.textColor, - this.fontScale, - this.padding = 3.0, - }); - - /// The icon to display. - final Icon icon; - - /// The text to display beside the icon. - final String? text; - - /// The color of the text. If null, defaults to the [ThemeData.textTheme.bodyMedium.color]. - final Color? textColor; - - /// The font scale to use for the text. - final FontScale? fontScale; - - /// The padding between the icon and the text. Defaults to 3.0. - final double padding; - - @override - Widget build(BuildContext context) { - if (text == null || text!.isEmpty) return icon; - final textStyle = TextStyle(color: textColor); - - return Row( - spacing: padding, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - icon, - ScalableText(text!, fontScale: fontScale, style: textStyle), - ], - ); - } -} diff --git a/lib/packages/ui/src/models/identity/name_style.dart b/lib/src/shared/identity/models/name_style.dart similarity index 100% rename from lib/packages/ui/src/models/identity/name_style.dart rename to lib/src/shared/identity/models/name_style.dart diff --git a/lib/src/shared/identity/utils/avatar_url.dart b/lib/src/shared/identity/utils/avatar_url.dart new file mode 100644 index 000000000..74e5cd52d --- /dev/null +++ b/lib/src/shared/identity/utils/avatar_url.dart @@ -0,0 +1,23 @@ +String? buildAvatarImageUrl( + String? sourceUrl, { + int? thumbnailSize, + String? format, +}) { + if (sourceUrl == null) return null; + + final queryParameters = {}; + if (thumbnailSize != null) { + queryParameters['thumbnail'] = thumbnailSize.toString(); + } + if (format != null) { + queryParameters['format'] = format; + } + + final imageUri = Uri.parse(sourceUrl); + + if (imageUri.path.contains('/pictrs/image/') && queryParameters.isNotEmpty) { + return Uri.https(imageUri.host, imageUri.path, queryParameters).toString(); + } + + return imageUri.toString(); +} diff --git a/lib/packages/ui/src/utils/identity/name_formatting.dart b/lib/src/shared/identity/utils/name_formatting.dart similarity index 61% rename from lib/packages/ui/src/utils/identity/name_formatting.dart rename to lib/src/shared/identity/utils/name_formatting.dart index a4dd2268c..969b98c03 100644 --- a/lib/packages/ui/src/utils/identity/name_formatting.dart +++ b/lib/src/shared/identity/utils/name_formatting.dart @@ -1,4 +1,4 @@ -import 'package:thunder/packages/ui/src/models/identity/name_style.dart'; +import 'package:thunder/src/shared/identity/models/name_style.dart'; String formatUserFullNamePrefix( String? name, @@ -26,19 +26,6 @@ String formatUserFullNameSuffix( }; } -String formatUserFullName( - String? name, - String? displayName, - String? instance, { - required FullNameSeparator separator, - required bool useDisplayName, -}) { - final prefix = formatUserFullNamePrefix(name, displayName, separator: separator, useDisplayName: useDisplayName); - final suffix = formatUserFullNameSuffix(instance, separator: separator); - - return '$prefix$suffix'; -} - String formatCommunityFullNamePrefix( String? name, String? displayName, { @@ -64,16 +51,3 @@ String formatCommunityFullNameSuffix( FullNameSeparator.lemmy => '@$instance', }; } - -String formatCommunityFullName( - String? name, - String? displayName, - String? instance, { - required FullNameSeparator separator, - required bool useDisplayName, -}) { - final prefix = formatCommunityFullNamePrefix(name, displayName, separator: separator, useDisplayName: useDisplayName); - final suffix = formatCommunityFullNameSuffix(instance, separator: separator); - - return '$prefix$suffix'; -} diff --git a/lib/src/shared/identity/widgets/avatars/community_avatar.dart b/lib/src/shared/identity/widgets/avatars/community_avatar.dart new file mode 100644 index 000000000..36637845f --- /dev/null +++ b/lib/src/shared/identity/widgets/avatars/community_avatar.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; + +import 'package:thunder/l10n/generated/app_localizations.dart'; +import 'package:thunder/packages/ui/ui.dart' show AvatarData, Avatar; +import 'package:thunder/src/foundation/primitives/models/thunder_community.dart'; +import 'package:thunder/src/shared/identity/utils/avatar_url.dart'; + +class CommunityAvatar extends StatelessWidget { + final ThunderCommunity community; + final double radius; + final bool showCommunityStatus; + final int? thumbnailSize; + final String? format; + + const CommunityAvatar({ + super.key, + required this.community, + this.radius = 12.0, + this.showCommunityStatus = false, + this.thumbnailSize, + this.format, + }); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final imageUrl = buildAvatarImageUrl( + community.icon, + thumbnailSize: thumbnailSize, + format: format, + ); + + final theme = Theme.of(context); + + return Stack( + children: [ + Avatar( + data: AvatarData( + fallbackLabel: community.titleOrName, + imageUrl: imageUrl, + radius: radius, + ), + ), + if (community.postingRestrictedToMods && showCommunityStatus) + Positioned( + bottom: -2.0, + right: -2.0, + child: Tooltip( + message: l10n.onlyModsCanPostInCommunity, + child: Container( + padding: const EdgeInsets.all(4.0), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + shape: BoxShape.circle, + ), + child: Icon( + Icons.lock, + color: theme.colorScheme.error, + size: 18.0, + semanticLabel: l10n.onlyModsCanPostInCommunity, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/features/identity/presentation/widgets/avatars/instance_avatar.dart b/lib/src/shared/identity/widgets/avatars/instance_avatar.dart similarity index 69% rename from lib/src/features/identity/presentation/widgets/avatars/instance_avatar.dart rename to lib/src/shared/identity/widgets/avatars/instance_avatar.dart index 3c412fb5d..6cc8e03ae 100644 --- a/lib/src/features/identity/presentation/widgets/avatars/instance_avatar.dart +++ b/lib/src/shared/identity/widgets/avatars/instance_avatar.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:thunder/packages/ui/ui.dart' as identity; -import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/packages/ui/ui.dart' show AvatarData, Avatar; +import 'package:thunder/src/foundation/primitives/models/thunder_instance_info.dart'; -/// App adapter for the generic identity package avatar. class InstanceAvatar extends StatelessWidget { final ThunderInstanceInfo instance; final double radius; @@ -22,8 +21,8 @@ class InstanceAvatar extends StatelessWidget { ? instance.domain : ''; - return identity.InstanceAvatar( - data: identity.AvatarData( + return Avatar( + data: AvatarData( fallbackLabel: fallbackLabel, imageUrl: instance.icon, radius: radius, diff --git a/lib/src/shared/identity/widgets/avatars/user_avatar.dart b/lib/src/shared/identity/widgets/avatars/user_avatar.dart new file mode 100644 index 000000000..c2342e728 --- /dev/null +++ b/lib/src/shared/identity/widgets/avatars/user_avatar.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +import 'package:thunder/packages/ui/ui.dart' show AvatarData, Avatar; +import 'package:thunder/src/foundation/primitives/models/thunder_user.dart'; +import 'package:thunder/src/shared/identity/utils/avatar_url.dart'; + +class UserAvatar extends StatelessWidget { + final ThunderUser user; + final double radius; + final int? thumbnailSize; + final String? format; + + const UserAvatar({ + super.key, + required this.user, + this.radius = 16.0, + this.thumbnailSize, + this.format, + }); + + @override + Widget build(BuildContext context) { + final imageUrl = buildAvatarImageUrl( + user.avatar, + thumbnailSize: thumbnailSize, + format: format, + ); + + return Avatar( + data: AvatarData( + fallbackLabel: user.displayNameOrName, + imageUrl: imageUrl, + radius: radius, + ), + ); + } +} diff --git a/lib/packages/ui/src/widgets/identity/full_name_widgets.dart b/lib/src/shared/identity/widgets/full_name_widgets.dart similarity index 56% rename from lib/packages/ui/src/widgets/identity/full_name_widgets.dart rename to lib/src/shared/identity/widgets/full_name_widgets.dart index fb1815248..6f5b98ed2 100644 --- a/lib/packages/ui/src/widgets/identity/full_name_widgets.dart +++ b/lib/src/shared/identity/widgets/full_name_widgets.dart @@ -1,13 +1,128 @@ import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:thunder/packages/ui/src/models/identity/name_style.dart'; -import 'package:thunder/packages/ui/src/utils/identity/name_formatting.dart'; +import 'package:thunder/src/foundation/primitives/primitives.dart'; +import 'package:thunder/src/features/settings/api.dart'; +import 'package:thunder/src/shared/identity/utils/name_formatting.dart'; -/// Package-generic full-name widget for users. class UserFullNameWidget extends StatelessWidget { const UserFullNameWidget({ + super.key, + this.name, + this.displayName, + this.instance, + this.userSeparator, + this.userNameThickness, + this.userNameColor, + this.instanceNameThickness, + this.instanceNameColor, + this.textStyle, + this.includeInstance = true, + this.fontScale, + this.autoSize = false, + this.transformColor, + this.useDisplayName, + }); + + final String? name; + final String? displayName; + final String? instance; + final FullNameSeparator? userSeparator; + final NameThickness? userNameThickness; + final NameColor? userNameColor; + final NameThickness? instanceNameThickness; + final NameColor? instanceNameColor; + final TextStyle? textStyle; + final bool includeInstance; + final FontScale? fontScale; + final bool autoSize; + final Color? Function(Color?)? transformColor; + final bool? useDisplayName; + + @override + Widget build(BuildContext context) { + final themePreferences = context.read().state; + + return CoreUserFullNameWidget( + name: name, + displayName: displayName, + instance: instance, + separator: userSeparator ?? themePreferences.userSeparator, + useDisplayName: useDisplayName ?? themePreferences.useDisplayNamesForUsers, + userNameThickness: userNameThickness ?? themePreferences.userFullNameUserNameThickness, + userNameColor: userNameColor ?? themePreferences.userFullNameUserNameColor, + instanceNameThickness: instanceNameThickness ?? themePreferences.userFullNameInstanceNameThickness, + instanceNameColor: instanceNameColor ?? themePreferences.userFullNameInstanceNameColor, + textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium, + includeInstance: includeInstance, + textScaleFactor: fontScale?.textScaleFactor ?? FontScale.base.textScaleFactor, + autoSize: autoSize, + transformColor: transformColor, + ); + } +} + +class CommunityFullNameWidget extends StatelessWidget { + const CommunityFullNameWidget({ + super.key, + this.name, + this.displayName, + this.instance, + this.communitySeparator, + this.communityNameThickness, + this.communityNameColor, + this.instanceNameThickness, + this.instanceNameColor, + this.textStyle, + this.includeInstance = true, + this.fontScale, + this.autoSize = false, + this.transformColor, + this.useDisplayName, + }); + + final String? name; + final String? displayName; + final String? instance; + final FullNameSeparator? communitySeparator; + final NameThickness? communityNameThickness; + final NameColor? communityNameColor; + final NameThickness? instanceNameThickness; + final NameColor? instanceNameColor; + final TextStyle? textStyle; + final bool includeInstance; + final FontScale? fontScale; + final bool autoSize; + final Color? Function(Color?)? transformColor; + final bool? useDisplayName; + + @override + Widget build(BuildContext context) { + final themePreferences = context.read().state; + + return CoreCommunityFullNameWidget( + name: name, + displayName: displayName, + instance: instance, + separator: communitySeparator ?? themePreferences.communitySeparator, + useDisplayName: useDisplayName ?? themePreferences.useDisplayNamesForCommunities, + communityNameThickness: communityNameThickness ?? themePreferences.communityFullNameCommunityNameThickness, + communityNameColor: communityNameColor ?? themePreferences.communityFullNameCommunityNameColor, + instanceNameThickness: instanceNameThickness ?? themePreferences.communityFullNameInstanceNameThickness, + instanceNameColor: instanceNameColor ?? themePreferences.communityFullNameInstanceNameColor, + textStyle: textStyle ?? Theme.of(context).textTheme.bodyMedium, + includeInstance: includeInstance, + textScaleFactor: fontScale?.textScaleFactor ?? FontScale.base.textScaleFactor, + autoSize: autoSize, + transformColor: transformColor, + ); + } +} + +class CoreUserFullNameWidget extends StatelessWidget { + const CoreUserFullNameWidget({ super.key, this.name, this.displayName, @@ -96,9 +211,8 @@ class UserFullNameWidget extends StatelessWidget { } } -/// Package-generic full-name widget for communities. -class CommunityFullNameWidget extends StatelessWidget { - const CommunityFullNameWidget({ +class CoreCommunityFullNameWidget extends StatelessWidget { + const CoreCommunityFullNameWidget({ super.key, this.name, this.displayName, diff --git a/lib/src/shared/input_dialogs.dart b/lib/src/shared/input_dialogs.dart index b4f3bb351..a2c2195db 100644 --- a/lib/src/shared/input_dialogs.dart +++ b/lib/src/shared/input_dialogs.dart @@ -1,10 +1,8 @@ import 'dart:async'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:collection/collection.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; @@ -15,16 +13,16 @@ import 'package:thunder/l10n/generated/app_localizations.dart'; import 'package:thunder/src/features/account/api.dart'; import 'package:thunder/src/features/feed/api.dart'; import 'package:thunder/src/features/search/api.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/shared/marquee_widget.dart'; import 'package:thunder/src/features/user/api.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/foundation/utils/utils.dart'; -import 'package:thunder/packages/ui/ui.dart' show showThunderDialog; +import 'package:thunder/packages/ui/ui.dart' show showThunderTypeaheadDialog; /// Shows a dialog which allows typing/search for a user void showUserInputDialog( @@ -63,10 +61,12 @@ void showUserInputDialog( return l10n.unableToFindUser; } - showInputDialog( + showThunderTypeaheadDialog( context: context, title: title, inputLabel: l10n.username, + primaryButtonText: l10n.ok, + secondaryButtonText: l10n.cancel, onSubmitted: onSubmitted, getSuggestions: (query) => getUserSuggestions(context, query: query, account: account), suggestionBuilder: (payload) => buildUserSuggestionWidget(context, payload), @@ -110,10 +110,9 @@ Widget buildUserSuggestionWidget(BuildContext context, ThunderUser payload, {voi backDuration: const Duration(seconds: 2), pauseDuration: const Duration(seconds: 1), child: UserFullNameWidget( - context, - payload.name, - payload.displayName, - fetchInstanceNameFromUrl(payload.actorId), + name: payload.name, + displayName: payload.displayName, + instance: fetchInstanceNameFromUrl(payload.actorId), // Override because we're showing display name above useDisplayName: false, ), @@ -178,10 +177,12 @@ void showCommunityInputDialog( return l10n.unableToFindCommunity; } - showInputDialog( + showThunderTypeaheadDialog( context: context, title: title, inputLabel: l10n.community, + primaryButtonText: l10n.ok, + secondaryButtonText: l10n.cancel, onSubmitted: onSubmitted, getSuggestions: (query) => getCommunitySuggestions(context, query: query, account: account, emptySuggestions: emptySuggestions, favoritedCommunities: favoritedCommunities), suggestionBuilder: (payload) => buildCommunitySuggestionWidget(context, payload), @@ -233,10 +234,9 @@ Widget buildCommunitySuggestionWidget(BuildContext context, ThunderCommunity pay backDuration: const Duration(seconds: 2), pauseDuration: const Duration(seconds: 1), child: CommunityFullNameWidget( - context, - payload.name, - payload.title, - fetchInstanceNameFromUrl(payload.actorId), + name: payload.name, + displayName: payload.title, + instance: fetchInstanceNameFromUrl(payload.actorId), // Override because we're showing display name above useDisplayName: false, ), @@ -305,10 +305,12 @@ void showInstanceInputDialog( } if (context.mounted) { - showInputDialog>( + showThunderTypeaheadDialog>( context: context, title: title, inputLabel: AppLocalizations.of(context)!.instance(1), + primaryButtonText: AppLocalizations.of(context)!.ok, + secondaryButtonText: AppLocalizations.of(context)!.cancel, onSubmitted: onSubmitted, getSuggestions: (query) => getInstanceSuggestions(query, linkedInstances), suggestionBuilder: (payload) => buildInstanceSuggestionWidget(payload, context: context), @@ -389,10 +391,12 @@ void showLanguageInputDialog(BuildContext context, } if (context.mounted) { - showInputDialog( + showThunderTypeaheadDialog( context: context, title: title, inputLabel: AppLocalizations.of(context)!.language, + primaryButtonText: AppLocalizations.of(context)!.ok, + secondaryButtonText: AppLocalizations.of(context)!.cancel, onSubmitted: onSubmitted, getSuggestions: (query) => getLanguageSuggestions(context, query, languages), suggestionBuilder: (payload) => buildLanguageSuggestionWidget(payload, context: context), @@ -455,86 +459,15 @@ void showKeywordInputDialog(BuildContext context, {required String title, requir } if (context.mounted) { - showInputDialog( + showThunderTypeaheadDialog( context: context, title: title, inputLabel: l10n.addKeywordFilter, + primaryButtonText: l10n.ok, + secondaryButtonText: l10n.cancel, onSubmitted: onSubmitted, getSuggestions: (query) => [], suggestionBuilder: (payload) => Container(), ); } } - -/// Shows a dialog which takes input and offers suggestions -void showInputDialog({ - required BuildContext context, - required String title, - required String inputLabel, - required Future Function({T? payload, String? value}) onSubmitted, - required FutureOr?> Function(String query) getSuggestions, - required Widget Function(T payload) suggestionBuilder, -}) async { - final textController = TextEditingController(); - // Capture our content widget's setState function so we can call it outside the widget - StateSetter? contentWidgetSetState; - String? contentWidgetError; - - await showThunderDialog( - context: context, - title: title, - onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(), - secondaryButtonText: AppLocalizations.of(context)!.cancel, - primaryButtonInitialEnabled: false, - onPrimaryButtonPressed: (dialogContext, setPrimaryButtonEnabled) async { - setPrimaryButtonEnabled(false); - final String? submitError = await onSubmitted(value: textController.text); - contentWidgetSetState?.call(() => contentWidgetError = submitError); - }, - primaryButtonText: AppLocalizations.of(context)!.ok, - // Use a stateful widget for the content so we can update the error message - contentWidgetBuilder: (setPrimaryButtonEnabled) => StatefulBuilder(builder: (context, setState) { - contentWidgetSetState = setState; - return SizedBox( - width: min(MediaQuery.of(context).size.width, 700), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TypeAheadField( - controller: textController, - builder: (context, controller, focusNode) => TextField( - controller: controller, - focusNode: focusNode, - onChanged: (value) { - setPrimaryButtonEnabled(value.trim().isNotEmpty); - setState(() => contentWidgetError = null); - }, - autofocus: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - labelText: inputLabel, - errorText: contentWidgetError, - ), - onSubmitted: (text) async { - setPrimaryButtonEnabled(false); - final String? submitError = await onSubmitted(value: text); - setState(() => contentWidgetError = submitError); - }, - ), - suggestionsCallback: getSuggestions, - itemBuilder: (context, payload) => suggestionBuilder(payload), - onSelected: (payload) async { - setPrimaryButtonEnabled(false); - final String? submitError = await onSubmitted(payload: payload); - setState(() => contentWidgetError = submitError); - }, - hideOnEmpty: true, - hideOnLoading: true, - hideOnError: true, - ), - ], - ), - ); - }), - ); -} diff --git a/lib/src/shared/link_information.dart b/lib/src/shared/link_information.dart deleted file mode 100644 index 183e60a05..000000000 --- a/lib/src/shared/link_information.dart +++ /dev/null @@ -1,118 +0,0 @@ -// Flutter imports -import 'package:flutter/material.dart'; - -// Project imports -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/app/shell/navigation/link_navigation_utils.dart'; -import 'package:thunder/src/shared/links/widgets/link_bottom_sheet.dart'; - -/// A widget that displays information about a link, including the link's media type if applicable. -/// -/// A custom [handleTapImage] callback can be provided to handle tap events on the link information. -class LinkInformation extends StatelessWidget { - /// URL of the link - final String? url; - - /// Type of media (image, link, text, etc.) - final MediaType? mediaType; - - /// The view mode of the media - final ViewMode viewMode; - - /// Whether to show edge to edge images. This is used to determine the border radius of the container. - /// TODO: Potentially fetch this from ThunderBloc. Doing so will affect other parts of the app (e.g., post body) - final bool showEdgeToEdgeImages; - - /// Custom callback function for when the link is tapped - final Function? onTap; - - /// Custom callback function for when the link is long-pressed - final Function? onLongPress; - - const LinkInformation({ - super.key, - this.url, - this.mediaType, - required this.viewMode, - this.showEdgeToEdgeImages = false, - this.onTap, - this.onLongPress, - }); - - /// Returns the appropriate icon for the media type - IconData _getIconForMediaType() { - return switch (mediaType) { - MediaType.image => Icons.image_outlined, - MediaType.video => Icons.play_arrow_rounded, - MediaType.text => Icons.wysiwyg_rounded, - _ => Icons.link_rounded, - }; - } - - /// Handles tap events on the link - void _handleTap(BuildContext context) { - if (onTap != null) { - onTap?.call(); - return; - } - - // Fallback to opening the link in the browser - handleLink(context, url: url!); - } - - /// Handles long press events on the link - void _handleLongPress(BuildContext context) { - if (onLongPress != null) { - onLongPress?.call(); - return; - } - - if (mediaType == MediaType.link) { - handleLinkLongPress(context, url!, url); - } - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final icon = _getIconForMediaType(); - - final borderRadius = BorderRadius.circular(showEdgeToEdgeImages ? 0 : 12); - - return Semantics( - link: true, - child: InkWell( - customBorder: RoundedRectangleBorder(borderRadius: borderRadius), - onTap: () => _handleTap(context), - onLongPress: () => _handleLongPress(context), - child: Container( - decoration: BoxDecoration( - borderRadius: borderRadius, - color: ElevationOverlay.applySurfaceTint( - theme.colorScheme.surface.withValues(alpha: 0.8), - theme.colorScheme.surfaceTint, - 10, - ), - ), - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Icon(icon, color: theme.colorScheme.onSecondaryContainer), - ), - if (viewMode != ViewMode.compact) - Expanded( - child: Text( - url!, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyMedium, - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/src/shared/links/links.dart b/lib/src/shared/links/links.dart deleted file mode 100644 index cd8ba0662..000000000 --- a/lib/src/shared/links/links.dart +++ /dev/null @@ -1 +0,0 @@ -export 'widgets/link_bottom_sheet.dart'; diff --git a/lib/src/shared/reply_to_preview_actions.dart b/lib/src/shared/reply_to_preview_actions.dart index ef98fe84b..59ade727c 100644 --- a/lib/src/shared/reply_to_preview_actions.dart +++ b/lib/src/shared/reply_to_preview_actions.dart @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:thunder/l10n/generated/app_localizations.dart'; -import 'package:thunder/src/shared/icon_text.dart'; -import 'package:thunder/packages/ui/ui.dart' show ThunderDivider, showSnackbar; +import 'package:thunder/packages/ui/ui.dart' show ThunderDivider, ThunderIconLabel, showSnackbar; /// Defines a widget which provides action buttons for the preview of a post or comment when replying /// @@ -44,10 +43,10 @@ class ReplyToPreviewActions extends StatelessWidget { onTap: onViewSourceToggled, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0), - child: IconText( - padding: 5.0, + child: ThunderIconLabel( + gap: 5.0, icon: Icon(Icons.edit_document, size: 15.0), - text: viewSource ? l10n.viewOriginal : l10n.viewSource, + label: viewSource ? l10n.viewOriginal : l10n.viewSource, ), ), ), @@ -59,10 +58,10 @@ class ReplyToPreviewActions extends StatelessWidget { }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0), - child: IconText( - padding: 5.0, + child: ThunderIconLabel( + gap: 5.0, icon: Icon(Icons.copy_rounded, size: 15.0), - text: l10n.copyText, + label: l10n.copyText, ), ), ), diff --git a/lib/src/shared/share/advanced_share_sheet.dart b/lib/src/shared/share/advanced_share_sheet.dart index bbccd4a1f..49f402ccb 100644 --- a/lib/src/shared/share/advanced_share_sheet.dart +++ b/lib/src/shared/share/advanced_share_sheet.dart @@ -10,8 +10,8 @@ import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/foundation/persistence/persistence.dart'; -import 'package:thunder/src/features/settings/api.dart'; -import 'package:thunder/src/shared/image_preview.dart'; +import 'package:thunder/src/shared/share/share_image_preview.dart'; +import 'package:thunder/packages/ui/ui.dart'; class AdvancedShareSheetOptions { AdvancedShareSheetOptions({ @@ -186,7 +186,7 @@ void showAdvancedShareSheet(BuildContext context, ThunderPost post) async { style: theme.textTheme.bodyMedium?.copyWith(fontStyle: FontStyle.italic), ), if (!_isImageCustomized(options, post) && options.includeImage && _hasImage(post)) - ImagePreview( + ShareImagePreview( url: post.media.first.thumbnailUrl.toString(), isExpandable: true, isComment: true, @@ -195,7 +195,7 @@ void showAdvancedShareSheet(BuildContext context, ThunderPost post) async { ), if (_isImageCustomized(options, post)) snapshot.hasData && !isGeneratingImage - ? ImagePreview( + ? ShareImagePreview( bytes: snapshot.data!, isExpandable: true, isComment: true, @@ -223,60 +223,52 @@ void showAdvancedShareSheet(BuildContext context, ThunderPost post) async { ), ), ), - ToggleOption( - description: AppLocalizations.of(context)!.includeTitle, - iconEnabled: Icons.title_rounded, - iconDisabled: Icons.title_rounded, - value: options.includeTitle, - onToggle: (_) => setState(() { - isGeneratingImage = true; - options.includeTitle = !options.includeTitle; - }), - highlightKey: null, - setting: null, - highlightedSetting: null, - ), - if (_hasImage(post)) - ToggleOption( - description: AppLocalizations.of(context)!.includeImage, - iconEnabled: Icons.image_rounded, - iconDisabled: Icons.image_rounded, - value: options.includeImage, - onToggle: (_) => setState(() { - isGeneratingImage = true; - options.includeImage = !options.includeImage; - }), + ThunderToggleOption( + title: AppLocalizations.of(context)!.includeTitle, + iconEnabled: Icons.title_rounded, + iconDisabled: Icons.title_rounded, + value: options.includeTitle, + onChanged: (_) => setState(() { + isGeneratingImage = true; + options.includeTitle = !options.includeTitle; + }), highlightKey: null, - setting: null, - highlightedSetting: null, - ), + highlighted: null == null), + if (_hasImage(post)) + ThunderToggleOption( + title: AppLocalizations.of(context)!.includeImage, + iconEnabled: Icons.image_rounded, + iconDisabled: Icons.image_rounded, + value: options.includeImage, + onChanged: (_) => setState(() { + isGeneratingImage = true; + options.includeImage = !options.includeImage; + }), + highlightKey: null, + highlighted: null == null), if (_hasText(post)) - ToggleOption( - description: AppLocalizations.of(context)!.includeText, - iconEnabled: Icons.comment_rounded, - iconDisabled: Icons.comment_rounded, - value: options.includeText, - onToggle: (_) => setState(() { - isGeneratingImage = true; - options.includeText = !options.includeText; - }), + ThunderToggleOption( + title: AppLocalizations.of(context)!.includeText, + iconEnabled: Icons.comment_rounded, + iconDisabled: Icons.comment_rounded, + value: options.includeText, + onChanged: (_) => setState(() { + isGeneratingImage = true; + options.includeText = !options.includeText; + }), + highlightKey: null, + highlighted: null == null), + ThunderToggleOption( + title: AppLocalizations.of(context)!.includeCommunity, + iconEnabled: Icons.people_rounded, + iconDisabled: Icons.people_rounded, + value: options.includeCommnity, + onChanged: (_) => setState(() { + isGeneratingImage = true; + options.includeCommnity = !options.includeCommnity; + }), highlightKey: null, - setting: null, - highlightedSetting: null, - ), - ToggleOption( - description: AppLocalizations.of(context)!.includeCommunity, - iconEnabled: Icons.people_rounded, - iconDisabled: Icons.people_rounded, - value: options.includeCommnity, - onToggle: (_) => setState(() { - isGeneratingImage = true; - options.includeCommnity = !options.includeCommnity; - }), - highlightKey: null, - setting: null, - highlightedSetting: null, - ), + highlighted: null == null), const SizedBox(height: 20), Align( alignment: Alignment.centerLeft, @@ -288,27 +280,23 @@ void showAdvancedShareSheet(BuildContext context, ThunderPost post) async { ), ), ), - ToggleOption( - description: AppLocalizations.of(context)!.includePostLink, - iconEnabled: Icons.link_rounded, - iconDisabled: Icons.link_rounded, - value: options.includePostLink, - onToggle: (_) => setState(() => options.includePostLink = !options.includePostLink), - highlightKey: null, - setting: null, - highlightedSetting: null, - ), - if (_hasExternalLink(post)) - ToggleOption( - description: AppLocalizations.of(context)!.includeExternalLink, + ThunderToggleOption( + title: AppLocalizations.of(context)!.includePostLink, iconEnabled: Icons.link_rounded, iconDisabled: Icons.link_rounded, - value: options.includeExternalLink, - onToggle: (_) => setState(() => options.includeExternalLink = !options.includeExternalLink), + value: options.includePostLink, + onChanged: (_) => setState(() => options.includePostLink = !options.includePostLink), highlightKey: null, - setting: null, - highlightedSetting: null, - ), + highlighted: null == null), + if (_hasExternalLink(post)) + ThunderToggleOption( + title: AppLocalizations.of(context)!.includeExternalLink, + iconEnabled: Icons.link_rounded, + iconDisabled: Icons.link_rounded, + value: options.includeExternalLink, + onChanged: (_) => setState(() => options.includeExternalLink = !options.includeExternalLink), + highlightKey: null, + highlighted: null == null), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, diff --git a/lib/src/shared/image_preview.dart b/lib/src/shared/share/share_image_preview.dart similarity index 96% rename from lib/src/shared/image_preview.dart rename to lib/src/shared/share/share_image_preview.dart index 3c97993da..18d584d6e 100644 --- a/lib/src/shared/image_preview.dart +++ b/lib/src/shared/share/share_image_preview.dart @@ -10,9 +10,9 @@ import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/app/state/thunder/thunder_bloc.dart'; import 'package:thunder/src/shared/theme/color_utils.dart'; -import 'package:thunder/src/features/content/presentation/widgets/media/media_utils.dart'; +import 'package:thunder/src/shared/content/utils/media/media_utils.dart'; -class ImagePreview extends StatefulWidget { +class ShareImagePreview extends StatefulWidget { final String? url; final Uint8List? bytes; final bool nsfw; @@ -29,7 +29,7 @@ class ImagePreview extends StatefulWidget { final bool? read; final String? altText; - const ImagePreview({ + const ShareImagePreview({ super.key, this.url, this.bytes, @@ -49,10 +49,10 @@ class ImagePreview extends StatefulWidget { }) : assert(url != null || bytes != null); @override - State createState() => _ImagePreviewState(); + State createState() => _ShareImagePreviewState(); } -class _ImagePreviewState extends State { +class _ShareImagePreviewState extends State { bool blur = false; double endBlur = 15; double startBlur = 0; diff --git a/lib/src/shared/widgets/chips/community_chip.dart b/lib/src/shared/widgets/chips/community_chip.dart index 8bd6c3f27..205f59c5b 100644 --- a/lib/src/shared/widgets/chips/community_chip.dart +++ b/lib/src/shared/widgets/chips/community_chip.dart @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/api.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/community_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/community_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/instance/domain/utils/instance_link_utils.dart'; import 'package:thunder/src/app/shell/navigation/navigation_utils.dart'; @@ -74,14 +74,12 @@ class CommunityChip extends StatelessWidget { children: [ if (showCommunityAvatar) Padding(padding: const EdgeInsets.only(top: 3, bottom: 3, right: 3), child: communityAvatar), CommunityFullNameWidget( - context, - communityName, - communityTitle, - fetchInstanceNameFromUrl(communityUrl), - includeInstance: includeInstance ?? postBodyShowCommunityInstance, - fontScale: metadataFontSizeScale, - transformColor: (color) => color?.withValues(alpha: 0.75), - ), + name: communityName, + displayName: communityTitle, + instance: fetchInstanceNameFromUrl(communityUrl), + includeInstance: includeInstance ?? postBodyShowCommunityInstance, + fontScale: metadataFontSizeScale, + transformColor: (color) => color?.withValues(alpha: 0.75)), ], ), ), diff --git a/lib/src/shared/widgets/chips/user_chip.dart b/lib/src/shared/widgets/chips/user_chip.dart index 099d07ed4..f6a0ab5a8 100644 --- a/lib/src/shared/widgets/chips/user_chip.dart +++ b/lib/src/shared/widgets/chips/user_chip.dart @@ -4,8 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; import 'package:thunder/src/features/feed/api.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/avatars/user_avatar.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/full_name_widgets.dart'; +import 'package:thunder/src/shared/identity/widgets/avatars/user_avatar.dart'; +import 'package:thunder/src/shared/identity/widgets/full_name_widgets.dart'; import 'package:thunder/src/features/comment/api.dart'; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/src/features/user/api.dart'; @@ -93,14 +93,12 @@ class UserChip extends StatelessWidget { ConstrainedBox( constraints: BoxConstraints(maxWidth: (constraints?.maxWidth ?? MediaQuery.sizeOf(context).width) * 0.55), child: UserFullNameWidget( - context, - user.name, - user.displayName, - fetchInstanceNameFromUrl(user.actorId), - includeInstance: includeInstance, - fontScale: metadataFontSizeScale, - transformColor: (c) => userGroups.isNotEmpty ? theme.textTheme.bodyMedium?.color : c?.withValues(alpha: opacity), - ), + name: user.name, + displayName: user.displayName, + instance: fetchInstanceNameFromUrl(user.actorId), + includeInstance: includeInstance, + fontScale: metadataFontSizeScale, + transformColor: (c) => userGroups.isNotEmpty ? theme.textTheme.bodyMedium?.color : c?.withValues(alpha: opacity)), ), if (userGroups.isNotEmpty) const SizedBox(width: 2.0), if (userGroups.contains(UserType.op)) diff --git a/lib/src/shared/widgets/media/media_view_text.dart b/lib/src/shared/widgets/media/media_view_text.dart deleted file mode 100644 index e0d7abe8e..000000000 --- a/lib/src/shared/widgets/media/media_view_text.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:flex_color_scheme/flex_color_scheme.dart'; -import 'package:html/parser.dart'; -import 'package:markdown/markdown.dart' hide Text; - -import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/notification/api.dart'; - -/// Creates a [MediaViewText] widget which displays a preview of the text content of a post. -/// -/// This widget should only be used when ViewMode is [ViewMode.compact] -class MediaViewText extends StatelessWidget { - /// The text content of the post. - final String? text; - - /// Whether the post has been read. This will affect the opacity of the text. - final bool? read; - - const MediaViewText({ - super.key, - this.text, - this.read, - }); - - /// Extracts plain text from markdown/HTML content. - /// TODO: Move this parsing outside of the UI layer to improve performance. - String? parseText() { - if (text?.isNotEmpty != true) return null; - - final htmlText = cleanImagesFromHtml(markdownToHtml(text!)); - return parse(parse(htmlText).body?.text).documentElement?.text ?? text; - } - - /// Calculates the optimal font size based on text length. - double _calculateFontSize(String text) { - return min(20, max(4.5, (20 * (1 / log(text.length))))); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - final baseColor = theme.colorScheme.onSecondaryContainer; - final readColor = baseColor.withValues(alpha: 0.55); - final unreadColor = baseColor.withValues(alpha: 0.7); - final color = read == true ? readColor : unreadColor; - - final plainText = parseText(); - - return ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Container( - height: ViewMode.compact.height, - width: ViewMode.compact.height, - color: theme.cardColor.darken(5), - child: plainText != null - ? Padding( - padding: const EdgeInsets.all(10.0), - child: Center( - child: Text( - plainText, - style: TextStyle( - fontSize: _calculateFontSize(plainText), - color: color, - ), - ), - ), - ) - : Icon(Icons.text_fields_rounded, color: color), - ), - ); - } -} diff --git a/lib/src/shared/widgets/text/selectable_text_modal.dart b/lib/src/shared/widgets/text/selectable_text_modal.dart index 3e704f7c5..04078ff55 100644 --- a/lib/src/shared/widgets/text/selectable_text_modal.dart +++ b/lib/src/shared/widgets/text/selectable_text_modal.dart @@ -8,8 +8,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/src/foundation/config/global_context.dart'; import 'package:thunder/src/foundation/primitives/primitives.dart'; -import 'package:thunder/src/features/content/presentation/widgets/common_markdown_body.dart'; -import 'package:thunder/src/features/identity/presentation/widgets/text/scalable_text.dart'; +import 'package:thunder/src/shared/content/widgets/markdown/common_markdown_body.dart'; +import 'package:thunder/packages/ui/ui.dart' show ScalableText; import 'package:thunder/src/features/settings/api.dart'; import 'package:thunder/packages/ui/ui.dart' show ThunderActionChip; @@ -133,7 +133,7 @@ void showSelectableTextModal(BuildContext context, {String? title, required Stri ? ScalableText( text, style: theme.textTheme.bodySmall?.copyWith(fontFamily: 'monospace'), - fontScale: contentFontSizeScale, + textScaleFactor: contentFontSizeScale.textScaleFactor, ) : CommonMarkdownBody( body: text,