[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.

center feed bar and fix indicator position (#60)

authored by

Davi Rodrigues and committed by
GitHub
17f6210b 6f8f9a95

+55 -87
+55 -87
lib/src/features/feed/ui/widgets/feed/feeds_bar.dart
··· 27 27 _logger = GetIt.I<LogService>().getLogger('FeedsBar'); 28 28 } 29 29 30 - double _calculateIndicatorPosition(dynamic settings) { 31 - final selectedIndex = settings.feeds.indexOf(settings.activeFeed); 32 - if (selectedIndex == -1) return 0; 33 - 34 - double position = 0; 35 - for (int i = 0; i < selectedIndex; i++) { 36 - position += _getTabWidth(settings.feeds[i].name) + 8; 37 - } 38 - 39 - final currentTabWidth = _getTabWidth(settings.activeFeed.name); 40 - position += currentTabWidth * 0.25; 41 - 42 - return position; 43 - } 44 - 45 - double _calculateIndicatorWidth(dynamic settings) { 46 - if (settings.activeFeed == null) return 0; 47 - return _getTabWidth(settings.activeFeed.name) * 0.5; 48 - } 49 - 50 30 double _getTabWidth(String text) { 51 31 final textPainter = TextPainter( 52 32 text: TextSpan( ··· 62 42 @override 63 43 Widget build(BuildContext context) { 64 44 final settings = ref.watch(settingsProvider); 45 + 46 + // Calculate total width of all tabs to center them 47 + double totalWidth = 0; 48 + for (final feed in settings.feeds) { 49 + totalWidth += _getTabWidth(feed.name) + 8.0; // 8.0 is margin 50 + } 51 + if (settings.feeds.isNotEmpty) totalWidth -= 8.0; // remove last margin 65 52 66 53 return AppBar( 67 54 backgroundColor: Colors.transparent, 68 55 elevation: 0, 56 + centerTitle: true, 69 57 flexibleSpace: Container( 70 58 decoration: const BoxDecoration( 71 59 gradient: LinearGradient( ··· 75 63 ), 76 64 ), 77 65 ), 78 - title: SizedBox( 79 - height: 40, 80 - child: Stack( 81 - children: [ 82 - ReorderableListView.builder( 66 + title: LayoutBuilder( 67 + builder: (context, constraints) { 68 + final availableWidth = constraints.maxWidth; 69 + final horizontalPadding = (availableWidth - totalWidth) / 2.0; 70 + 71 + return SizedBox( 72 + height: 44, 73 + child: ReorderableListView.builder( 74 + padding: EdgeInsets.symmetric(horizontal: horizontalPadding > 0 ? horizontalPadding : 0), 83 75 scrollDirection: Axis.horizontal, 84 76 itemCount: settings.feeds.length, 85 77 onReorderStart: (index) { ··· 93 85 }); 94 86 }, 95 87 onReorder: (oldIndex, newIndex) async { 96 - // Prevent multiple simultaneous reorders 97 88 if (_isReordering) return; 98 - 99 - setState(() { 100 - _isReordering = true; 101 - }); 102 - 89 + setState(() => _isReordering = true); 103 90 try { 104 - // Adjust newIndex for Flutter's reordering behavior 105 - if (newIndex > oldIndex) { 106 - newIndex -= 1; 107 - } 108 - 109 - // Remember which feed was active before reordering 91 + if (newIndex > oldIndex) newIndex -= 1; 110 92 final activeFeedBeforeReorder = settings.activeFeed; 111 - 112 - // Perform the reorder operation 113 93 await ref.read(settingsProvider.notifier).reorderFeed(oldIndex, newIndex); 114 - 115 - // Wait for the settings to update 116 94 await Future.delayed(const Duration(milliseconds: 50)); 117 - 118 - // Get updated settings and find new position of active feed 119 95 final updatedSettings = ref.read(settingsProvider); 120 96 final newActiveFeedIndex = updatedSettings.feeds.indexOf(activeFeedBeforeReorder); 121 - 122 - // Only update page controller if we found the active feed and it moved 123 97 if (newActiveFeedIndex != -1 && widget.pageController.hasClients) { 124 - final currentPage = widget.pageController.page?.round() ?? 0; 125 - if (currentPage != newActiveFeedIndex) { 98 + if ((widget.pageController.page?.round() ?? 0) != newActiveFeedIndex) { 126 99 widget.pageController.jumpToPage(newActiveFeedIndex); 127 100 } 128 101 } 129 102 } catch (e) { 130 103 _logger.e('Error reordering feeds: $e'); 131 104 } finally { 132 - setState(() { 133 - _isReordering = false; 134 - }); 105 + setState(() => _isReordering = false); 135 106 } 136 107 }, 137 108 proxyDecorator: (child, index, animation) { ··· 158 129 itemBuilder: (context, index) { 159 130 final feed = settings.feeds[index]; 160 131 final isSelected = settings.activeFeed == feed; 161 - 162 - return Container( 132 + return InkWell( 163 133 key: ValueKey(feed.identifier), 164 - margin: const EdgeInsets.only(right: 8.0), 165 - child: InkWell( 166 - onTap: _isReordering 167 - ? null 168 - : () { 169 - // Prevent tap during reordering 170 - ref.read(settingsProvider.notifier).setActiveFeed(feed); 171 - final feedIndex = settings.feeds.indexOf(feed); 172 - if (feedIndex != -1 && widget.pageController.hasClients) { 173 - widget.pageController.jumpToPage(feedIndex); 174 - } 175 - }, 176 - borderRadius: BorderRadius.circular(25), 177 - child: Container( 178 - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), 179 - child: Text( 180 - feed.name, 181 - style: TextStyle( 182 - color: isSelected ? Colors.white : Colors.white.withAlpha(140), 183 - fontWeight: FontWeight.w700, 184 - fontSize: 18, 134 + onTap: _isReordering 135 + ? null 136 + : () { 137 + ref.read(settingsProvider.notifier).setActiveFeed(feed); 138 + final feedIndex = settings.feeds.indexOf(feed); 139 + if (feedIndex != -1 && widget.pageController.hasClients) { 140 + widget.pageController.jumpToPage(feedIndex); 141 + } 142 + }, 143 + borderRadius: BorderRadius.circular(25), 144 + child: Padding( 145 + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), 146 + child: Column( 147 + mainAxisSize: MainAxisSize.min, 148 + children: [ 149 + Text( 150 + feed.name, 151 + style: TextStyle( 152 + color: isSelected ? Colors.white : Colors.white.withAlpha(140), 153 + fontWeight: FontWeight.w700, 154 + fontSize: 18, 155 + ), 156 + ), 157 + const SizedBox(height: 4), 158 + AnimatedContainer( 159 + duration: const Duration(milliseconds: 300), 160 + curve: Curves.easeInOutCubic, 161 + height: 2, 162 + width: isSelected ? _getTabWidth(feed.name) * 0.5 : 0, 163 + decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(1)), 185 164 ), 186 - ), 165 + ], 187 166 ), 188 167 ), 189 168 ); 190 169 }, 191 170 ), 192 - AnimatedPositioned( 193 - duration: const Duration(milliseconds: 300), 194 - curve: Curves.easeInOutCubic, 195 - bottom: 0, 196 - left: _calculateIndicatorPosition(settings), 197 - width: _calculateIndicatorWidth(settings), 198 - height: 2, 199 - child: Container( 200 - decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(1)), 201 - ), 202 - ), 203 - ], 204 - ), 171 + ); 172 + }, 205 173 ), 206 174 actions: [ 207 175 // IconButton(