import "dart:typed_data"; import "package:flutter/foundation.dart"; import "package:go_router/go_router.dart"; import "package:intl/intl.dart"; import "package:provider/provider.dart"; import "package:quotegen_client/models/gallery_item.dart"; import "package:quotegen_client/providers/form_provider.dart"; import "package:quotegen_client/providers/gallery_provider.dart"; import "package:quotegen_client/providers/session_provider.dart"; import "package:quotegen_client/widgets/tweet_template_widget.dart"; import "package:shadcn_flutter/shadcn_flutter.dart"; class GalleryPage extends StatelessWidget { static GoRoute route = GoRoute( path: "/gallery", builder: (context, state) => GalleryPage(), ); @override Widget build(BuildContext context) { if (kIsWeb) { return Scaffold( child: Center(child: Text("Gallery not available on web")), ); } final gallery = context.watch(); final double topPadding = MediaQuery.of(context).padding.top; if (gallery.isLoading) { return Scaffold( child: Center(child: CircularProgressIndicator()), ); } if (gallery.items.isEmpty) { return Scaffold( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( LucideIcons.galleryHorizontal, size: 48, ), Gap(12), Text("No saved posts yet").large.semiBold, Gap(4), Text("Use the bookmark button in the editor to save a post").small.muted, ], ), ), ); } return Scaffold( child: CustomScrollView( slivers: [ SliverToBoxAdapter( child: Gap(topPadding + 14), ), SliverPadding( padding: EdgeInsets.symmetric(horizontal: 14), sliver: SliverGrid.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 0.75, ), itemCount: gallery.items.length, itemBuilder: (context, index) { final item = gallery.items[index]; return _GalleryCard( item: item, avatarBytes: gallery.decodeAvatar(item), postImageBytes: gallery.decodePostImage(item), ); }, ), ), SliverToBoxAdapter( child: Gap(120), ), ], ), ); } } class _GalleryCard extends StatelessWidget { final GalleryItem item; final Uint8List? avatarBytes; final Uint8List? postImageBytes; const _GalleryCard({ required this.item, this.avatarBytes, this.postImageBytes, }); @override Widget build(BuildContext context) { final ts = DateTime.fromMillisecondsSinceEpoch(item.createdAt); final formatted = DateFormat("MMM d, yyyy").format(ts); final engagement = (item.replies == "" && item.retweets == "" && item.likes == "" && item.views == "") ? null : { "replies": int.tryParse(item.replies.replaceAll(",", "")) ?? 0, "retweets": int.tryParse(item.retweets.replaceAll(",", "")) ?? 0, "likes": int.tryParse(item.likes.replaceAll(",", "")) ?? 0, "views": int.tryParse(item.views.replaceAll(",", "")) ?? 0, }; return GestureDetector( onTap: () => _onTap(context), child: OutlinedContainer( clipBehavior: Clip.hardEdge, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( child: IgnorePointer( child: TweetTemplateWidget( displayName: item.displayName, username: item.handle, isVerified: item.verified, text: item.content, timestamp: DateTime.fromMillisecondsSinceEpoch(item.timestamp * 1000), avatarBytes: avatarBytes, imageBytes: postImageBytes, engagement: engagement, ), ), ), Divider(), Padding( padding: EdgeInsets.symmetric(horizontal: 10, vertical: 6), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(item.displayName.isEmpty ? "Untitled" : item.displayName).small.semiBold, Text(formatted).xSmall.muted, ], ), ), IconButton.destructive( icon: Icon(LucideIcons.trash2, size: 14), onPressed: () { context.read().deleteItem(item.id); }, ), ], ), ), ], ), ), ); } Future _onTap(BuildContext context) async { final form = context.read(); final session = context.read(); final gallery = context.read(); bool hasContent = form.displayName.isNotEmpty || form.handle.isNotEmpty || form.content.isNotEmpty || form.avatarImage != null || form.postImage != null; if (hasContent) { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text("Load this post?"), content: Text("Your current editor content will be replaced."), actions: [ Button.outline( child: Text("Cancel"), onPressed: () => Navigator.of(ctx).pop(false), ), Button.primary( child: Text("Load"), onPressed: () => Navigator.of(ctx).pop(true), ), ], ), ); if (confirmed != true) return; } // load item fields into form form.setDisplayName(item.displayName); form.setHandle(item.handle); form.setContent(item.content); form.setVerified(item.verified); final dt = DateTime.fromMillisecondsSinceEpoch(item.timestamp * 1000); form.setDate(DateTime(dt.year, dt.month, dt.day)); form.setTime(TimeOfDay(hour: dt.hour, minute: dt.minute)); form.setAvatarImage(gallery.decodeAvatar(item)); form.setPostImage(gallery.decodePostImage(item)); form.setReplies(item.replies); form.setRetweets(item.retweets); form.setLikes(item.likes); form.setViews(item.views); await session.initializeSession(form.buildFullRequest()); if (context.mounted) { context.go("/"); } } }