mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
3
fork

Configure Feed

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

at main 157 lines 5.5 kB view raw
1import 'package:bluesky/app_bsky_graph_defs.dart' as bsky_graph; 2import 'package:flutter/material.dart'; 3import 'package:flutter_animate/flutter_animate.dart'; 4import 'package:flutter_bloc/flutter_bloc.dart'; 5import 'package:go_router/go_router.dart'; 6import 'package:lazurite/core/theme/animation_tokens.dart'; 7import 'package:lazurite/core/theme/animation_utils.dart'; 8import 'package:lazurite/features/auth/bloc/auth_bloc.dart'; 9import 'package:lazurite/features/lists/cubit/my_lists_cubit.dart'; 10import 'package:lazurite/features/lists/data/list_repository.dart'; 11import 'package:lazurite/features/lists/presentation/widgets/create_edit_list_dialog.dart'; 12import 'package:lazurite/features/lists/presentation/widgets/list_row_tile.dart'; 13import 'package:lazurite/shared/presentation/widgets/animated_refresh_indicator.dart'; 14import 'package:lazurite/shared/presentation/widgets/empty_state.dart'; 15import 'package:lazurite/shared/presentation/widgets/error_state.dart'; 16import 'package:lazurite/shared/presentation/widgets/loading_state.dart'; 17import 'package:lazurite/shared/presentation/widgets/staggered_entrance.dart'; 18 19class MyListsScreen extends StatelessWidget { 20 const MyListsScreen({super.key}); 21 22 @override 23 Widget build(BuildContext context) { 24 final actor = context.read<AuthBloc>().state.tokens?.did ?? ''; 25 return BlocProvider( 26 create: (context) => MyListsCubit(listRepository: context.read<ListRepository>())..load(actor: actor), 27 child: const _MyListsView(), 28 ); 29 } 30} 31 32class _MyListsView extends StatefulWidget { 33 const _MyListsView(); 34 35 @override 36 State<_MyListsView> createState() => _MyListsViewState(); 37} 38 39class _MyListsViewState extends State<_MyListsView> with SingleTickerProviderStateMixin { 40 late final TabController _tabController; 41 final Set<String> _seenListUris = <String>{}; 42 43 @override 44 void initState() { 45 super.initState(); 46 _tabController = TabController(length: 2, vsync: this); 47 } 48 49 @override 50 void dispose() { 51 _tabController.dispose(); 52 super.dispose(); 53 } 54 55 Future<void> _showCreateDialog(BuildContext context) async { 56 final result = await showDialog<CreateEditListResult>( 57 context: context, 58 builder: (_) => const CreateEditListDialog(), 59 ); 60 61 if (result == null || !context.mounted) return; 62 63 final authState = context.read<AuthBloc>().state; 64 final userDid = authState.tokens?.did; 65 if (userDid == null) return; 66 67 final listUri = await context.read<MyListsCubit>().createList( 68 userDid: userDid, 69 name: result.name, 70 purpose: result.purpose, 71 description: result.description, 72 avatarBytes: result.avatarBytes, 73 avatarMimeType: result.avatarMimeType, 74 ); 75 76 if (listUri != null && context.mounted) { 77 await context.push('/list?uri=${Uri.encodeComponent(listUri.toString())}'); 78 } 79 } 80 81 @override 82 Widget build(BuildContext context) { 83 return Scaffold( 84 appBar: AppBar( 85 title: const Text('My Lists'), 86 bottom: TabBar( 87 controller: _tabController, 88 tabs: const [ 89 Tab(text: 'FEEDS'), 90 Tab(text: 'MODERATION'), 91 ], 92 labelStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 2.2), 93 unselectedLabelStyle: const TextStyle(fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 2.2), 94 indicatorWeight: 2, 95 ), 96 ), 97 body: BlocBuilder<MyListsCubit, MyListsState>( 98 builder: (context, state) { 99 if (state.status == MyListsStatus.loading) { 100 return const LoadingState(); 101 } 102 103 if (state.status == MyListsStatus.error) { 104 return ErrorState( 105 title: 'Failed to load lists', 106 message: state.errorMessage ?? 'Unknown error', 107 onRetry: () => context.read<MyListsCubit>().refresh(), 108 ); 109 } 110 111 return TabBarView( 112 controller: _tabController, 113 children: [_buildListTab(context, state.curationLists), _buildListTab(context, state.moderationLists)], 114 ); 115 }, 116 ), 117 floatingActionButton: 118 FloatingActionButton( 119 heroTag: 'my-lists-fab', 120 onPressed: () => _showCreateDialog(context), 121 child: const Icon(Icons.add), 122 ).animateIfAllowed( 123 context, 124 effects: const [ 125 FadeEffect(duration: Anim.feedItem, curve: Anim.enter), 126 ScaleEffect(begin: Offset(0, 0), end: Offset(1, 1), duration: Anim.feedItem, curve: Anim.emphasis), 127 ], 128 ), 129 ); 130 } 131 132 Widget _buildListTab(BuildContext context, List<bsky_graph.ListView> lists) { 133 if (lists.isEmpty) { 134 return const EmptyState(message: 'No lists yet', icon: Icons.list_alt_outlined); 135 } 136 137 return AnimatedRefreshIndicator( 138 onRefresh: () => context.read<MyListsCubit>().refresh(), 139 child: ListView.builder( 140 itemCount: lists.length, 141 itemBuilder: (context, index) { 142 final list = lists[index]; 143 return StaggeredEntrance( 144 itemKey: list.uri.toString(), 145 index: index, 146 seenKeys: _seenListUris, 147 child: ListRowTile( 148 key: ValueKey(list.uri), 149 list: list, 150 onTap: () => context.push('/list?uri=${Uri.encodeComponent(list.uri.toString())}'), 151 ), 152 ); 153 }, 154 ), 155 ); 156 } 157}