···
import '../../models/post.dart';
import '../../providers/auth_provider.dart';
import '../../providers/feed_provider.dart';
8
+
import '../../widgets/icons/bluesky_icons.dart';
import '../../widgets/post_card.dart';
11
+
/// Header layout constants
12
+
const double _kHeaderHeight = 44;
13
+
const double _kTabUnderlineWidth = 28;
14
+
const double _kTabUnderlineHeight = 3;
15
+
const double _kHeaderContentPadding = _kHeaderHeight;
class FeedScreen extends StatefulWidget {
11
-
const FeedScreen({super.key});
18
+
const FeedScreen({super.key, this.onSearchTap});
20
+
/// Callback when search icon is tapped (to switch to communities tab)
21
+
final VoidCallback? onSearchTap;
State<FeedScreen> createState() => _FeedScreenState();
···
final isLoading = context.select<FeedProvider, bool>((p) => p.isLoading);
final error = context.select<FeedProvider, String?>((p) => p.error);
76
+
final feedType = context.select<FeedProvider, FeedType>(
// IMPORTANT: This relies on FeedProvider creating new list instances
// (_posts = [..._posts, ...response.feed]) rather than mutating in-place.
···
backgroundColor: AppColors.background,
84
-
backgroundColor: AppColors.background,
85
-
foregroundColor: AppColors.textPrimary,
86
-
title: Text(isAuthenticated ? 'Feed' : 'Explore'),
87
-
automaticallyImplyLeading: false,
91
-
isLoading: isLoading,
94
-
isLoadingMore: isLoadingMore,
95
-
isAuthenticated: isAuthenticated,
96
-
currentTime: currentTime,
99
+
// Feed content (behind header)
101
+
isLoading: isLoading,
104
+
isLoadingMore: isLoadingMore,
105
+
isAuthenticated: isAuthenticated,
106
+
currentTime: currentTime,
108
+
// Transparent header overlay
110
+
feedType: feedType,
111
+
isAuthenticated: isAuthenticated,
119
+
Widget _buildHeader({
120
+
required FeedType feedType,
121
+
required bool isAuthenticated,
124
+
height: _kHeaderHeight,
125
+
decoration: BoxDecoration(
126
+
// Gradient fade from solid to transparent
127
+
gradient: LinearGradient(
128
+
begin: Alignment.topCenter,
129
+
end: Alignment.bottomCenter,
131
+
AppColors.background,
132
+
AppColors.background.withValues(alpha: 0.8),
133
+
AppColors.background.withValues(alpha: 0),
135
+
stops: const [0.0, 0.6, 1.0],
138
+
padding: const EdgeInsets.symmetric(horizontal: 16),
141
+
// Feed type tabs in the center
143
+
child: _buildFeedTypeTabs(
144
+
feedType: feedType,
145
+
isAuthenticated: isAuthenticated,
148
+
// Search/Communities icon on the right
149
+
if (widget.onSearchTap != null)
151
+
label: 'Navigate to Communities',
154
+
onTap: widget.onSearchTap,
155
+
borderRadius: BorderRadius.circular(20),
156
+
splashColor: AppColors.primary.withValues(alpha: 0.2),
158
+
padding: const EdgeInsets.all(8),
159
+
child: BlueSkyIcon.search(color: AppColors.textPrimary),
168
+
Widget _buildFeedTypeTabs({
169
+
required FeedType feedType,
170
+
required bool isAuthenticated,
172
+
// If not authenticated, only show Discover
173
+
if (!isAuthenticated) {
175
+
child: _buildFeedTypeTab(
183
+
// Authenticated: show both tabs side by side (TikTok style)
185
+
mainAxisAlignment: MainAxisAlignment.center,
189
+
isActive: feedType == FeedType.discover,
190
+
onTap: () => _switchToFeedType(FeedType.discover),
192
+
const SizedBox(width: 24),
195
+
isActive: feedType == FeedType.forYou,
196
+
onTap: () => _switchToFeedType(FeedType.forYou),
202
+
Widget _buildFeedTypeTab({
203
+
required String label,
204
+
required bool isActive,
205
+
required VoidCallback? onTap,
208
+
label: '$label feed${isActive ? ', selected' : ''}',
210
+
selected: isActive,
211
+
child: GestureDetector(
213
+
behavior: HitTestBehavior.opaque,
215
+
mainAxisSize: MainAxisSize.min,
216
+
mainAxisAlignment: MainAxisAlignment.center,
222
+
? AppColors.textPrimary
223
+
: AppColors.textSecondary.withValues(alpha: 0.6),
225
+
fontWeight: isActive ? FontWeight.w700 : FontWeight.w400,
228
+
const SizedBox(height: 2),
229
+
// Underline indicator (TikTok style)
231
+
width: _kTabUnderlineWidth,
232
+
height: _kTabUnderlineHeight,
233
+
decoration: BoxDecoration(
234
+
color: isActive ? AppColors.textPrimary : Colors.transparent,
235
+
borderRadius: BorderRadius.circular(2),
244
+
void _switchToFeedType(FeedType type) {
245
+
Provider.of<FeedProvider>(context, listen: false).setFeedType(type);
···
color: AppColors.primary,
controller: _scrollController,
354
+
// Add top padding so content isn't hidden behind transparent header
355
+
padding: const EdgeInsets.only(top: _kHeaderContentPadding),
// Add extra item for loading indicator or pagination error
itemCount: posts.length + (isLoadingMore || error != null ? 1 : 0),
itemBuilder: (context, index) {