[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(feed): add reordering & long press support in FeedTagList

+394 -161
+114 -6
lib/src/core/design_system/components/molecules/feed_tag_list.dart
··· 1 + import 'dart:ui' show lerpDouble; 2 + 1 3 import 'package:flutter/material.dart'; 4 + import 'package:flutter/services.dart'; 2 5 import 'package:spark/src/core/design_system/components/atoms/tags/feed_tag.dart'; 3 6 7 + /// Data class representing a feed tag with additional metadata for operations 8 + class FeedTagData { 9 + const FeedTagData({ 10 + required this.id, 11 + required this.text, 12 + this.isTimeline = false, 13 + this.isLiked = false, 14 + this.canDelete = true, 15 + }); 16 + 17 + final String id; 18 + final String text; 19 + final bool isTimeline; 20 + final bool isLiked; 21 + final bool canDelete; 22 + } 23 + 4 24 class FeedTagList extends StatefulWidget { 5 25 const FeedTagList({ 6 26 required this.tags, 7 27 super.key, 8 28 this.selectedTagId, 9 29 this.onTagTap, 30 + this.onReorder, 31 + this.onLongPress, 32 + this.enableReordering = false, 10 33 }); 11 34 12 - final List<({String id, String text})> tags; 35 + final List<FeedTagData> tags; 13 36 final String? selectedTagId; 14 37 final Function(String tagId)? onTagTap; 38 + final Function(int oldIndex, int newIndex)? onReorder; 39 + final Function(FeedTagData tag)? onLongPress; 40 + final bool enableReordering; 15 41 16 42 @override 17 43 State<FeedTagList> createState() => _FeedTagListState(); ··· 19 45 20 46 class _FeedTagListState extends State<FeedTagList> { 21 47 String? _selectedTagId; 48 + bool _isReordering = false; 22 49 23 50 @override 24 51 void initState() { ··· 37 64 } 38 65 39 66 void _handleTagTap(String tagId) { 67 + if (_isReordering) return; 40 68 setState(() { 41 69 _selectedTagId = tagId; 42 70 }); 43 71 widget.onTagTap?.call(tagId); 44 72 } 45 73 74 + void _handleLongPress(FeedTagData tag) { 75 + HapticFeedback.mediumImpact(); 76 + widget.onLongPress?.call(tag); 77 + } 78 + 46 79 @override 47 80 Widget build(BuildContext context) { 81 + if (widget.enableReordering && widget.onReorder != null) { 82 + return SizedBox( 83 + height: 30, 84 + child: ReorderableListView.builder( 85 + scrollDirection: Axis.horizontal, 86 + buildDefaultDragHandles: false, 87 + onReorderStart: (index) { 88 + HapticFeedback.mediumImpact(); 89 + setState(() { 90 + _isReordering = true; 91 + }); 92 + }, 93 + onReorderEnd: (index) { 94 + setState(() { 95 + _isReordering = false; 96 + }); 97 + }, 98 + onReorder: (oldIndex, newIndex) { 99 + if (newIndex > oldIndex) newIndex -= 1; 100 + widget.onReorder?.call(oldIndex, newIndex); 101 + }, 102 + proxyDecorator: (child, index, animation) { 103 + return AnimatedBuilder( 104 + animation: animation, 105 + builder: (context, child) { 106 + final animValue = Curves.easeInOutCubic.transform( 107 + animation.value, 108 + ); 109 + final scale = lerpDouble(1, 1.1, animValue)!; 110 + 111 + return Transform.scale( 112 + scale: scale, 113 + child: Material( 114 + color: Colors.transparent, 115 + child: child, 116 + ), 117 + ); 118 + }, 119 + child: child, 120 + ); 121 + }, 122 + itemCount: widget.tags.length, 123 + itemBuilder: (context, index) { 124 + final tag = widget.tags[index]; 125 + return Padding( 126 + key: ValueKey(tag.id), 127 + padding: EdgeInsets.only( 128 + right: index < widget.tags.length - 1 ? 30 : 0, 129 + ), 130 + child: ReorderableDelayedDragStartListener( 131 + index: index, 132 + child: GestureDetector( 133 + onLongPress: widget.onLongPress != null 134 + ? () => _handleLongPress(tag) 135 + : null, 136 + child: FeedTag( 137 + id: tag.id, 138 + text: tag.text, 139 + selected: _selectedTagId == tag.id, 140 + onTap: () => _handleTagTap(tag.id), 141 + ), 142 + ), 143 + ), 144 + ); 145 + }, 146 + ), 147 + ); 148 + } 149 + 150 + // Non-reorderable version with long press support 48 151 return SizedBox( 49 152 height: 30, 50 153 child: ListView.separated( ··· 53 156 itemCount: widget.tags.length, 54 157 itemBuilder: (context, index) { 55 158 final tag = widget.tags[index]; 56 - return FeedTag( 57 - id: tag.id, 58 - text: tag.text, 59 - selected: _selectedTagId == tag.id, 60 - onTap: () => _handleTagTap(tag.id), 159 + return GestureDetector( 160 + onLongPress: widget.onLongPress != null 161 + ? () => _handleLongPress(tag) 162 + : null, 163 + child: FeedTag( 164 + id: tag.id, 165 + text: tag.text, 166 + selected: _selectedTagId == tag.id, 167 + onTap: () => _handleTagTap(tag.id), 168 + ), 61 169 ); 62 170 }, 63 171 ),
+10 -1
lib/src/core/design_system/templates/feeds_bar_template.dart
··· 10 10 required this.tags, 11 11 this.selectedTagId, 12 12 this.onTagTap, 13 + this.onReorder, 14 + this.onLongPress, 15 + this.enableReordering = false, 13 16 this.action, 14 17 this.height = kToolbarHeight, 15 18 super.key, 16 19 }); 17 20 18 - final List<({String id, String text})> tags; 21 + final List<FeedTagData> tags; 19 22 final String? selectedTagId; 20 23 final ValueChanged<String>? onTagTap; 24 + final Function(int oldIndex, int newIndex)? onReorder; 25 + final Function(FeedTagData tag)? onLongPress; 26 + final bool enableReordering; 21 27 final Widget? action; 22 28 final double height; 23 29 ··· 53 59 tags: tags, 54 60 selectedTagId: selectedTagId, 55 61 onTagTap: onTagTap, 62 + onReorder: onReorder, 63 + onLongPress: onLongPress, 64 + enableReordering: enableReordering, 56 65 ), 57 66 ), 58 67 if (action != null) ...[
+1 -5
lib/src/core/routing/app_router.dart
··· 85 85 AutoRoute(page: EditProfileRoute.page, path: '/profile-editor'), 86 86 87 87 AutoRoute(page: SettingsRoute.page, path: '/settings'), 88 + AutoRoute(page: FeedListRoute.page, path: '/settings/feeds'), 88 89 AutoRoute(page: LabelerManagementRoute.page, path: '/settings/labelers'), 89 90 AutoRoute(page: BlocksRoute.page, path: '/settings/blocks'), 90 91 ··· 103 104 AutoRoute(page: CommentsListRoute.page, path: '', initial: true), 104 105 AutoRoute(page: RepliesRoute.page, path: 'replies/:postUri'), 105 106 ], 106 - ), 107 - CustomRoute( 108 - page: FeedSettingsRoute.page, 109 - path: '/feed-settings', 110 - customRouteBuilder: feedSettingsBuilder, 111 107 ), 112 108 CustomRoute( 113 109 page: LabelerLabelSettingsRoute.page,
-1
lib/src/core/routing/pages.dart
··· 25 25 export 'package:spark/src/features/profile/ui/pages/user_profile_page.dart'; 26 26 export 'package:spark/src/features/search/ui/pages/search_page.dart'; 27 27 export 'package:spark/src/features/settings/ui/pages/feed_list_page.dart'; 28 - export 'package:spark/src/features/settings/ui/pages/feed_settings_page.dart'; 29 28 export 'package:spark/src/features/settings/ui/pages/labeler_label_settings_page.dart'; 30 29 export 'package:spark/src/features/settings/ui/pages/labeler_management_page.dart'; 31 30 export 'package:spark/src/features/settings/ui/pages/settings_page.dart';
+23 -8
lib/src/features/feed/ui/pages/feeds_page.dart
··· 35 35 super.dispose(); 36 36 } 37 37 38 - void _updatePageController(List<Feed> feeds, Feed activeFeed) { 38 + void _updatePageController( 39 + List<Feed> feeds, 40 + Feed activeFeed, { 41 + bool forceJump = false, 42 + }) { 39 43 if (_isPageControllerUpdating) return; 40 44 41 45 final activeIndex = feeds.indexOf(activeFeed); ··· 58 62 if (!_pageController!.hasClients) return; 59 63 60 64 final currentPage = _pageController!.page?.round() ?? 0; 61 - if (currentPage != activeIndex && activeIndex >= 0) { 65 + if ((currentPage != activeIndex || forceJump) && activeIndex >= 0) { 62 66 _isPageControllerUpdating = true; 63 67 WidgetsBinding.instance.addPostFrameCallback((_) { 64 68 if (mounted && _pageController!.hasClients) { ··· 69 73 } 70 74 } 71 75 76 + /// Check if feeds list order has changed 77 + bool _feedsOrderChanged(List<Feed> newFeeds) { 78 + if (_lastFeedsList == null) return true; 79 + if (_lastFeedsList!.length != newFeeds.length) return true; 80 + 81 + for (var i = 0; i < newFeeds.length; i++) { 82 + if (_lastFeedsList![i].config.id != newFeeds[i].config.id) { 83 + return true; 84 + } 85 + } 86 + return false; 87 + } 88 + 72 89 @override 73 90 Widget build(BuildContext context) { 74 91 final settings = ref.watch(settingsProvider); ··· 102 119 // Check if we need to initialize or update the page controller 103 120 final needsInitialization = !_isInitialized; 104 121 final activeFeedChanged = _lastActiveFeed != activeFeed; 105 - final feedsListChanged = 106 - _lastFeedsList == null || 107 - _lastFeedsList!.length != feeds.length || 108 - !_lastFeedsList!.every(feeds.contains); 122 + final feedsOrderChanged = _feedsOrderChanged(feeds); 109 123 110 - if (needsInitialization || activeFeedChanged || feedsListChanged) { 111 - _updatePageController(feeds, activeFeed); 124 + if (needsInitialization || activeFeedChanged || feedsOrderChanged) { 125 + // Force jump when order changes to ensure we stay on the active feed 126 + _updatePageController(feeds, activeFeed, forceJump: feedsOrderChanged); 112 127 _isInitialized = true; 113 128 _lastActiveFeed = activeFeed; 114 129 _lastFeedsList = List.from(feeds); // Create a copy
+175 -17
lib/src/features/feed/ui/widgets/feed/feeds_bar.dart
··· 1 - import 'package:auto_route/auto_route.dart'; 2 1 import 'package:flutter/material.dart'; 3 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 3 import 'package:spark/src/core/design_system/components/atoms/icons.dart'; 4 + import 'package:spark/src/core/design_system/components/molecules/create_media_sheet.dart'; 5 + import 'package:spark/src/core/design_system/components/molecules/feed_tag_list.dart'; 5 6 import 'package:spark/src/core/design_system/templates/feeds_bar_template.dart'; 6 - import 'package:spark/src/core/routing/app_router.dart'; 7 + import 'package:spark/src/core/media/create_media_actions.dart'; 8 + import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 7 9 import 'package:spark/src/features/feed/providers/feed_refresh_trigger_provider.dart'; 8 10 import 'package:spark/src/features/settings/providers/settings_provider.dart'; 9 11 ··· 20 22 } 21 23 22 24 class _FeedsBarState extends ConsumerState<FeedsBar> { 25 + void _showCreateMenu(BuildContext context) { 26 + showCreateMediaSheet( 27 + context, 28 + onRecord: CreateMediaActions.onRecord(context, storyMode: false), 29 + onUploadVideo: CreateMediaActions.onUploadVideo( 30 + context, 31 + storyMode: false, 32 + ), 33 + onUploadImages: CreateMediaActions.onUploadImages( 34 + context, 35 + storyMode: false, 36 + ), 37 + ); 38 + } 39 + 40 + void _showFeedOptionsSheet(BuildContext context, Feed feed) { 41 + final isTimeline = 42 + feed.type == 'timeline' && feed.config.value == 'following'; 43 + final isLiked = feed.view?.viewer?.like != null; 44 + final canDelete = !isTimeline; 45 + final canLike = feed.view != null; // Only non-timeline feeds can be liked 46 + 47 + showModalBottomSheet<void>( 48 + context: context, 49 + backgroundColor: Theme.of(context).colorScheme.surface, 50 + shape: const RoundedRectangleBorder( 51 + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), 52 + ), 53 + builder: (context) { 54 + return SafeArea( 55 + child: Padding( 56 + padding: const EdgeInsets.symmetric(vertical: 8), 57 + child: Column( 58 + mainAxisSize: MainAxisSize.min, 59 + children: [ 60 + // Handle indicator 61 + Container( 62 + width: 40, 63 + height: 4, 64 + margin: const EdgeInsets.only(bottom: 16), 65 + decoration: BoxDecoration( 66 + color: Theme.of(context) 67 + .colorScheme 68 + .onSurface 69 + .withAlpha(50), 70 + borderRadius: BorderRadius.circular(2), 71 + ), 72 + ), 73 + // Feed name header 74 + Padding( 75 + padding: const EdgeInsets.symmetric( 76 + horizontal: 16, 77 + vertical: 8, 78 + ), 79 + child: Text( 80 + feed.view?.displayName ?? 'Following', 81 + style: Theme.of(context).textTheme.titleMedium?.copyWith( 82 + fontWeight: FontWeight.bold, 83 + ), 84 + ), 85 + ), 86 + const Divider(), 87 + // Like/Unlike option 88 + if (canLike) 89 + ListTile( 90 + leading: Icon( 91 + isLiked ? Icons.favorite : Icons.favorite_border, 92 + color: isLiked ? Colors.red : null, 93 + ), 94 + title: Text(isLiked ? 'Unlike Feed' : 'Like Feed'), 95 + onTap: () async { 96 + Navigator.pop(context); 97 + if (isLiked) { 98 + await ref 99 + .read(settingsProvider.notifier) 100 + .unlikeFeed(feed); 101 + } else { 102 + await ref 103 + .read(settingsProvider.notifier) 104 + .likeFeed(feed); 105 + } 106 + }, 107 + ), 108 + // Delete option 109 + if (canDelete) 110 + ListTile( 111 + leading: const Icon( 112 + Icons.delete_outline, 113 + color: Colors.red, 114 + ), 115 + title: const Text( 116 + 'Remove Feed', 117 + style: TextStyle(color: Colors.red), 118 + ), 119 + onTap: () async { 120 + Navigator.pop(context); 121 + // Show confirmation dialog 122 + final confirmed = await showDialog<bool>( 123 + context: context, 124 + builder: (context) => AlertDialog( 125 + title: const Text('Remove Feed'), 126 + content: Text( 127 + 'Are you sure you want to remove ' 128 + '"${feed.view?.displayName ?? 'this feed'}"?', 129 + ), 130 + actions: [ 131 + TextButton( 132 + onPressed: () => Navigator.pop(context, false), 133 + child: const Text('Cancel'), 134 + ), 135 + TextButton( 136 + onPressed: () => Navigator.pop(context, true), 137 + style: TextButton.styleFrom( 138 + foregroundColor: Colors.red, 139 + ), 140 + child: const Text('Remove'), 141 + ), 142 + ], 143 + ), 144 + ); 145 + if (confirmed == true) { 146 + await ref 147 + .read(settingsProvider.notifier) 148 + .removeFeed(feed); 149 + } 150 + }, 151 + ), 152 + // Cancel option 153 + ListTile( 154 + leading: const Icon(Icons.close), 155 + title: const Text('Cancel'), 156 + onTap: () => Navigator.pop(context), 157 + ), 158 + ], 159 + ), 160 + ), 161 + ); 162 + }, 163 + ); 164 + } 165 + 23 166 @override 24 167 Widget build(BuildContext context) { 25 168 final settings = ref.watch(settingsProvider); 26 169 27 170 // Only show pinned feeds in the home view 28 - final pinnedFeeds = settings.feeds 29 - .where((feed) => feed.config.pinned) 30 - .toList(); 171 + final pinnedFeeds = 172 + settings.feeds.where((feed) => feed.config.pinned).toList(); 31 173 32 - final tags = pinnedFeeds 33 - .map( 34 - (feed) => ( 35 - id: feed.config.id, 36 - text: feed.view != null ? feed.view!.displayName : 'Following', 37 - ), 38 - ) 39 - .toList(); 174 + final tags = pinnedFeeds.map((feed) { 175 + final isTimeline = 176 + feed.type == 'timeline' && feed.config.value == 'following'; 177 + return FeedTagData( 178 + id: feed.config.id, 179 + text: feed.view != null ? feed.view!.displayName : 'Following', 180 + isTimeline: isTimeline, 181 + isLiked: feed.view?.viewer?.like != null, 182 + canDelete: !isTimeline, 183 + ); 184 + }).toList(); 40 185 41 186 return FeedsBarTemplate( 42 187 tags: tags, ··· 56 201 } 57 202 } 58 203 }, 59 - action: IconButton( 60 - icon: AppIcons.hashtag(), 61 - padding: const EdgeInsets.all(5), 62 - onPressed: () => context.router.navigate(const FeedSettingsRoute()), 204 + onLongPress: (tagData) { 205 + final feed = pinnedFeeds.firstWhere( 206 + (f) => f.config.id == tagData.id, 207 + ); 208 + _showFeedOptionsSheet(context, feed); 209 + }, 210 + action: SizedBox( 211 + width: 40, 212 + height: 40, 213 + child: IconButton( 214 + padding: EdgeInsets.zero, 215 + constraints: const BoxConstraints(), 216 + splashColor: Colors.transparent, 217 + highlightColor: Colors.transparent, 218 + onPressed: () => _showCreateMenu(context), 219 + icon: AppIcons.addPostFilled(size: 30), 220 + ), 63 221 ), 64 222 ); 65 223 }
+30 -37
lib/src/features/settings/ui/pages/feed_list_page.dart
··· 3 3 import 'package:auto_route/auto_route.dart'; 4 4 import 'package:flutter/material.dart'; 5 5 import 'package:flutter_riverpod/flutter_riverpod.dart'; 6 + import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 6 7 import 'package:spark/src/core/design_system/components/molecules/settings_feed_card.dart'; 7 8 import 'package:spark/src/core/network/atproto/data/models/feed_models.dart'; 8 9 import 'package:spark/src/features/settings/providers/settings_provider.dart'; ··· 33 34 34 35 return Scaffold( 35 36 backgroundColor: colorScheme.surface, 37 + appBar: AppBar( 38 + backgroundColor: colorScheme.surface, 39 + elevation: 0, 40 + scrolledUnderElevation: 0, 41 + leading: const AppLeadingButton(), 42 + title: const Text('Your Feeds'), 43 + centerTitle: true, 44 + actions: [ 45 + TextButton.icon( 46 + onPressed: () { 47 + setState(() { 48 + _isEditMode = !_isEditMode; 49 + }); 50 + }, 51 + icon: Icon(_isEditMode ? Icons.check : Icons.edit, size: 18), 52 + label: Text( 53 + _isEditMode ? 'Done' : 'Edit', 54 + style: const TextStyle(fontSize: 14), 55 + ), 56 + style: TextButton.styleFrom( 57 + foregroundColor: colorScheme.primary, 58 + padding: const EdgeInsets.symmetric( 59 + horizontal: 12, 60 + vertical: 8, 61 + ), 62 + ), 63 + ), 64 + ], 65 + ), 36 66 body: Column( 37 67 children: [ 38 - // Feeds List Header with Edit Toggle 39 - Padding( 40 - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 41 - child: Row( 42 - children: [ 43 - Text( 44 - 'Your Feeds', 45 - style: TextStyle( 46 - fontSize: 18, 47 - fontWeight: FontWeight.bold, 48 - color: colorScheme.onSurface, 49 - ), 50 - ), 51 - const Spacer(), 52 - TextButton.icon( 53 - onPressed: () { 54 - setState(() { 55 - _isEditMode = !_isEditMode; 56 - }); 57 - }, 58 - icon: Icon(_isEditMode ? Icons.check : Icons.edit, size: 18), 59 - label: Text( 60 - _isEditMode ? 'Done' : 'Edit', 61 - style: const TextStyle(fontSize: 14), 62 - ), 63 - style: TextButton.styleFrom( 64 - foregroundColor: colorScheme.primary, 65 - padding: const EdgeInsets.symmetric( 66 - horizontal: 12, 67 - vertical: 8, 68 - ), 69 - ), 70 - ), 71 - ], 72 - ), 73 - ), 74 - const SizedBox(height: 8), 75 68 // Feeds List 76 69 Expanded( 77 70 child: ReorderableListView.builder(
-84
lib/src/features/settings/ui/pages/feed_settings_page.dart
··· 1 - import 'package:auto_route/auto_route.dart'; 2 - import 'package:flutter/material.dart'; 3 - import 'package:flutter_riverpod/flutter_riverpod.dart'; 4 - import 'package:spark/src/core/design_system/components/atoms/buttons/app_leading_button.dart'; 5 - import 'package:spark/src/features/home/providers/feed_settings_visibility_provider.dart'; 6 - import 'package:spark/src/features/settings/ui/pages/feed_list_page.dart'; 7 - import 'package:spark/src/features/settings/ui/pages/labeler_management_page.dart'; 8 - 9 - @RoutePage() 10 - class FeedSettingsPage extends ConsumerStatefulWidget { 11 - const FeedSettingsPage({super.key}); 12 - 13 - @override 14 - ConsumerState<FeedSettingsPage> createState() => _FeedSettingsPageState(); 15 - } 16 - 17 - class _FeedSettingsPageState extends ConsumerState<FeedSettingsPage> 18 - with SingleTickerProviderStateMixin { 19 - late TabController _tabController; 20 - FeedSettingsVisibility? _visibilityNotifier; 21 - 22 - @override 23 - void initState() { 24 - super.initState(); 25 - _tabController = TabController(length: 2, vsync: this); 26 - // Store notifier reference & mark feed settings as visible when page opens 27 - // Use post-frame callback to avoid modifying provider during build 28 - WidgetsBinding.instance.addPostFrameCallback((_) { 29 - if (mounted) { 30 - _visibilityNotifier = ref.read(feedSettingsVisibilityProvider.notifier); 31 - _visibilityNotifier?.setVisible(true); 32 - } 33 - }); 34 - } 35 - 36 - @override 37 - void dispose() { 38 - // Mark feed settings as not visible when page closes 39 - // Use Future to delay the modification until after dispose completes 40 - final notifier = _visibilityNotifier; 41 - Future(() { 42 - notifier?.setVisible(false); 43 - }); 44 - _tabController.dispose(); 45 - super.dispose(); 46 - } 47 - 48 - @override 49 - Widget build(BuildContext context) { 50 - final theme = Theme.of(context); 51 - final colorScheme = theme.colorScheme; 52 - 53 - return Scaffold( 54 - backgroundColor: colorScheme.surface, 55 - appBar: AppBar( 56 - elevation: 0, 57 - scrolledUnderElevation: 0, 58 - backgroundColor: colorScheme.surface, 59 - foregroundColor: colorScheme.onSurface, 60 - iconTheme: IconThemeData(color: colorScheme.onSurface), 61 - titleTextStyle: theme.appBarTheme.titleTextStyle?.copyWith( 62 - color: colorScheme.onSurface, 63 - ), 64 - title: const Text('Feed Settings'), 65 - centerTitle: true, 66 - leading: const AppLeadingButton(), 67 - bottom: TabBar( 68 - controller: _tabController, 69 - tabs: const [ 70 - Tab(text: 'Your Feeds'), 71 - Tab(text: 'Labelers'), 72 - ], 73 - ), 74 - ), 75 - body: TabBarView( 76 - controller: _tabController, 77 - children: const [ 78 - FeedListPage(), 79 - LabelerManagementPage(), 80 - ], 81 - ), 82 - ); 83 - } 84 - }
+23
lib/src/features/settings/ui/pages/settings_page.dart
··· 218 218 ), 219 219 child: ListTile( 220 220 title: const Text( 221 + 'Feeds', 222 + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 223 + ), 224 + trailing: const Icon(FluentIcons.list_24_regular), 225 + onTap: () => context.router.push(const FeedListRoute()), 226 + contentPadding: const EdgeInsets.symmetric( 227 + horizontal: 16, 228 + vertical: 4, 229 + ), 230 + ), 231 + ), 232 + ), 233 + Padding( 234 + padding: const EdgeInsets.symmetric(vertical: 8), 235 + child: Container( 236 + decoration: BoxDecoration( 237 + color: Theme.of( 238 + context, 239 + ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), 240 + borderRadius: BorderRadius.circular(12), 241 + ), 242 + child: ListTile( 243 + title: const Text( 221 244 'Labelers', 222 245 style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), 223 246 ),
+18 -2
widgetbook/lib/molecules/feed_tag_list.dart
··· 19 19 max: tagCount - 1, 20 20 divisions: tagCount - 1, 21 21 ); 22 - final tags = List.generate(tagCount, (i) => (id: 'tag_$i', text: 'Tag $i')); 22 + final enableReordering = context.knobs.boolean( 23 + label: 'enable_reordering', 24 + initialValue: true, 25 + ); 26 + final tags = List.generate( 27 + tagCount, 28 + (i) => FeedTagData( 29 + id: 'tag_$i', 30 + text: 'Tag $i', 31 + isTimeline: i == 0, 32 + canDelete: i != 0, 33 + ), 34 + ); 23 35 return Center( 24 36 child: Container( 25 37 constraints: BoxConstraints( ··· 33 45 child: FeedTagList( 34 46 tags: tags, 35 47 selectedTagId: tags[selectedIndex].id, 36 - onTagTap: (id) => print('Tag tapped: $id'), 48 + enableReordering: enableReordering, 49 + onTagTap: (id) => debugPrint('Tag tapped: $id'), 50 + onReorder: (oldIndex, newIndex) => 51 + debugPrint('Reorder: $oldIndex -> $newIndex'), 52 + onLongPress: (tag) => debugPrint('Long press: ${tag.text}'), 37 53 ), 38 54 ), 39 55 );