this repo has no description

feat: implement hashtag navigation across comments, gallery, and profile pages

+100 -15
+5 -3
lib/screens/comments_page.dart
··· 4 4 import 'package:grain/models/comment.dart'; 5 5 import 'package:grain/models/gallery.dart'; 6 6 import 'package:grain/models/gallery_photo.dart'; 7 + import 'package:grain/screens/hashtag_page.dart'; 7 8 import 'package:grain/screens/profile_page.dart'; 8 9 import 'package:grain/utils.dart'; 9 10 import 'package:grain/widgets/app_image.dart'; ··· 505 506 // context, 506 507 // ).push(MaterialPageRoute(builder: (context) => WebViewPage(url: url))); 507 508 }, 508 - onTagTap: (tag) { 509 - // TODO: Implement hashtag navigation 510 - }, 509 + onTagTap: (tag) => Navigator.push( 510 + context, 511 + MaterialPageRoute(builder: (_) => HashtagPage(hashtag: tag)), 512 + ), 511 513 ), 512 514 if (comment.focus != null && 513 515 ((comment.focus!.thumb?.isNotEmpty ?? false) ||
+5 -3
lib/screens/gallery_page.dart
··· 4 4 import 'package:grain/models/gallery_photo.dart'; 5 5 import 'package:grain/providers/gallery_cache_provider.dart'; 6 6 import 'package:grain/screens/create_gallery_page.dart'; 7 + import 'package:grain/screens/hashtag_page.dart'; 7 8 import 'package:grain/screens/profile_page.dart'; 8 9 import 'package:grain/widgets/app_image.dart'; 9 10 import 'package:grain/widgets/faceted_text.dart'; ··· 237 238 onLinkTap: (url) { 238 239 // TODO: Implement or use your WebViewPage 239 240 }, 240 - onTagTap: (tag) { 241 - // TODO: Implement hashtag navigation 242 - }, 241 + onTagTap: (tag) => Navigator.push( 242 + context, 243 + MaterialPageRoute(builder: (_) => HashtagPage(hashtag: tag)), 244 + ), 243 245 ), 244 246 ), 245 247 if (isLoggedIn)
+67
lib/screens/hashtag_page.dart
··· 1 + import 'package:flutter/material.dart'; 2 + import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 + import 'package:grain/api.dart'; 4 + import 'package:grain/widgets/timeline_item.dart'; 5 + 6 + import '../providers/gallery_cache_provider.dart'; 7 + 8 + class HashtagPage extends ConsumerStatefulWidget { 9 + final String hashtag; 10 + const HashtagPage({super.key, required this.hashtag}); 11 + 12 + @override 13 + ConsumerState<HashtagPage> createState() => _HashtagPageState(); 14 + } 15 + 16 + class _HashtagPageState extends ConsumerState<HashtagPage> { 17 + List<String> _uris = []; 18 + bool _loading = true; 19 + 20 + @override 21 + void initState() { 22 + super.initState(); 23 + _fetchGalleries(); 24 + } 25 + 26 + Future<void> _fetchGalleries() async { 27 + setState(() { 28 + _loading = true; 29 + }); 30 + final galleries = await apiService.getTimeline(algorithm: 'hashtag_${widget.hashtag}'); 31 + ref.read(galleryCacheProvider.notifier).setGalleries(galleries); 32 + setState(() { 33 + _uris = galleries.map((g) => g.uri).toList(); 34 + _loading = false; 35 + }); 36 + } 37 + 38 + @override 39 + Widget build(BuildContext context) { 40 + final theme = Theme.of(context); 41 + return Scaffold( 42 + appBar: AppBar( 43 + bottom: PreferredSize( 44 + preferredSize: const Size.fromHeight(1), 45 + child: Container(color: theme.dividerColor, height: 1), 46 + ), 47 + backgroundColor: theme.appBarTheme.backgroundColor, 48 + surfaceTintColor: theme.appBarTheme.backgroundColor, 49 + elevation: 0.5, 50 + title: Text('#${widget.hashtag}', style: theme.appBarTheme.titleTextStyle), 51 + ), 52 + body: _loading 53 + ? Center( 54 + child: CircularProgressIndicator(strokeWidth: 2, color: theme.colorScheme.primary), 55 + ) 56 + : _uris.isEmpty 57 + ? Center(child: Text('No galleries found for #${widget.hashtag}')) 58 + : ListView.builder( 59 + padding: const EdgeInsets.only(top: 8, bottom: 80), 60 + itemCount: _uris.length, 61 + itemBuilder: (context, index) { 62 + return TimelineItemWidget(galleryUri: _uris[index]); 63 + }, 64 + ), 65 + ); 66 + } 67 + }
-1
lib/screens/home_page.dart
··· 48 48 }); 49 49 try { 50 50 final galleries = await apiService.getTimeline(algorithm: algorithm); 51 - print("Fetched following timeline: ${galleries.length} items"); 52 51 container.read(galleryCacheProvider.notifier).setGalleries(galleries); 53 52 setState(() { 54 53 _followingTimelineUris = galleries.map((g) => g.uri).toList();
+5 -3
lib/screens/profile_page.dart
··· 3 3 import 'package:grain/api.dart'; 4 4 import 'package:grain/app_theme.dart'; 5 5 import 'package:grain/models/gallery.dart'; 6 + import 'package:grain/screens/hashtag_page.dart'; 6 7 import 'package:grain/widgets/app_image.dart'; 7 8 import 'package:grain/widgets/faceted_text.dart'; 8 9 ··· 245 246 onLinkTap: (url) { 246 247 // TODO: Implement WebViewPage navigation 247 248 }, 248 - onTagTap: (tag) { 249 - // TODO: Implement hashtag navigation 250 - }, 249 + onTagTap: (tag) => Navigator.push( 250 + context, 251 + MaterialPageRoute(builder: (_) => HashtagPage(hashtag: tag)), 252 + ), 251 253 linkStyle: TextStyle( 252 254 color: Theme.of(context).colorScheme.primary, 253 255 fontWeight: FontWeight.w600,
+18 -5
lib/widgets/timeline_item.dart
··· 1 1 import 'package:flutter/material.dart'; 2 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 3 import 'package:grain/api.dart'; 4 + import 'package:grain/screens/hashtag_page.dart'; 4 5 import 'package:grain/utils.dart'; 5 6 import 'package:grain/widgets/app_image.dart'; 7 + import 'package:grain/widgets/faceted_text.dart'; 6 8 import 'package:grain/widgets/gallery_action_buttons.dart'; 7 9 import 'package:grain/widgets/gallery_preview.dart'; 8 10 ··· 133 135 if (gallery.description?.isNotEmpty == true) 134 136 Padding( 135 137 padding: const EdgeInsets.only(top: 4, left: 8, right: 8), 136 - child: Text( 137 - gallery.description ?? '', 138 - style: theme.textTheme.bodySmall?.copyWith( 139 - fontSize: 13, 140 - color: theme.colorScheme.onSurfaceVariant, 138 + child: FacetedText( 139 + text: gallery.description ?? '', 140 + facets: gallery.facets, 141 + style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant), 142 + linkStyle: theme.textTheme.bodySmall?.copyWith( 143 + color: theme.colorScheme.primary, 144 + fontWeight: FontWeight.w600, 145 + ), 146 + onMentionTap: (did) { 147 + Navigator.of( 148 + context, 149 + ).push(MaterialPageRoute(builder: (context) => ProfilePage(did: did))); 150 + }, 151 + onTagTap: (tag) => Navigator.push( 152 + context, 153 + MaterialPageRoute(builder: (_) => HashtagPage(hashtag: tag)), 141 154 ), 142 155 ), 143 156 ),