285 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.dart';
 | |
| import 'package:gap/gap.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/services/compose_storage_db.dart';
 | |
| import 'package:island/widgets/alert.dart';
 | |
| import 'package:island/widgets/content/sheet.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| 
 | |
| class DraftManagerSheet extends HookConsumerWidget {
 | |
|   final Function(String draftId)? onDraftSelected;
 | |
| 
 | |
|   const DraftManagerSheet({super.key, this.onDraftSelected});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final theme = Theme.of(context);
 | |
|     final colorScheme = theme.colorScheme;
 | |
|     final searchController = useTextEditingController();
 | |
|     final searchQuery = useState('');
 | |
| 
 | |
|     final drafts = ref.watch(composeStorageNotifierProvider);
 | |
| 
 | |
|     // Search functionality
 | |
|     final filteredDrafts = useMemoized(() {
 | |
|       if (searchQuery.value.isEmpty) {
 | |
|         return drafts.values.toList()
 | |
|           ..sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!));
 | |
|       }
 | |
| 
 | |
|       final query = searchQuery.value.toLowerCase();
 | |
|       return drafts.values.where((draft) {
 | |
|           return (draft.title?.toLowerCase().contains(query) ?? false) ||
 | |
|               (draft.description?.toLowerCase().contains(query) ?? false) ||
 | |
|               (draft.content?.toLowerCase().contains(query) ?? false);
 | |
|         }).toList()
 | |
|         ..sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!));
 | |
|     }, [drafts, searchQuery.value]);
 | |
| 
 | |
|     return SheetScaffold(
 | |
|       titleText: 'drafts'.tr(),
 | |
|       child: Column(
 | |
|         children: [
 | |
|           // Search bar
 | |
|           Padding(
 | |
|             padding: const EdgeInsets.all(16),
 | |
|             child: TextField(
 | |
|               controller: searchController,
 | |
|               decoration: InputDecoration(
 | |
|                 hintText: 'searchDrafts'.tr(),
 | |
|                 prefixIcon: const Icon(Symbols.search),
 | |
|                 border: OutlineInputBorder(
 | |
|                   borderRadius: BorderRadius.circular(12),
 | |
|                 ),
 | |
|                 contentPadding: const EdgeInsets.symmetric(
 | |
|                   horizontal: 16,
 | |
|                   vertical: 12,
 | |
|                 ),
 | |
|               ),
 | |
|               onChanged: (value) => searchQuery.value = value,
 | |
|             ),
 | |
|           ),
 | |
| 
 | |
|           // Drafts list
 | |
|           if (filteredDrafts.isEmpty)
 | |
|             Expanded(
 | |
|               child: Center(
 | |
|                 child: Column(
 | |
|                   mainAxisAlignment: MainAxisAlignment.center,
 | |
|                   children: [
 | |
|                     Icon(
 | |
|                       Symbols.draft,
 | |
|                       size: 64,
 | |
|                       color: colorScheme.onSurface.withOpacity(0.3),
 | |
|                     ),
 | |
|                     const Gap(16),
 | |
|                     Text(
 | |
|                       searchQuery.value.isEmpty
 | |
|                           ? 'noDrafts'.tr()
 | |
|                           : 'noSearchResults'.tr(),
 | |
|                       style: theme.textTheme.bodyLarge?.copyWith(
 | |
|                         color: colorScheme.onSurface.withOpacity(0.6),
 | |
|                       ),
 | |
|                     ),
 | |
|                   ],
 | |
|                 ),
 | |
|               ),
 | |
|             )
 | |
|           else
 | |
|             Expanded(
 | |
|               child: ListView.builder(
 | |
|                 itemCount: filteredDrafts.length,
 | |
|                 itemBuilder: (context, index) {
 | |
|                   final draft = filteredDrafts[index];
 | |
|                   return _DraftItem(
 | |
|                     draft: draft,
 | |
|                     onTap: () {
 | |
|                       Navigator.of(context).pop();
 | |
|                       onDraftSelected?.call(draft.id);
 | |
|                     },
 | |
|                     onDelete: () async {
 | |
|                       await ref
 | |
|                           .read(composeStorageNotifierProvider.notifier)
 | |
|                           .deleteDraft(draft.id);
 | |
|                     },
 | |
|                   );
 | |
|                 },
 | |
|               ),
 | |
|             ),
 | |
| 
 | |
|           // Clear all button
 | |
|           if (filteredDrafts.isNotEmpty) ...[
 | |
|             const Divider(),
 | |
|             Padding(
 | |
|               padding: const EdgeInsets.all(16),
 | |
|               child: Row(
 | |
|                 children: [
 | |
|                   Expanded(
 | |
|                     child: OutlinedButton.icon(
 | |
|                       onPressed: () async {
 | |
|                         final confirmed = await showConfirmAlert(
 | |
|                           'clearAllDraftsConfirm'.tr(),
 | |
|                           'clearAllDrafts'.tr(),
 | |
|                         );
 | |
| 
 | |
|                         if (confirmed == true) {
 | |
|                           await ref
 | |
|                               .read(composeStorageNotifierProvider.notifier)
 | |
|                               .clearAllDrafts();
 | |
|                         }
 | |
|                       },
 | |
|                       icon: const Icon(Symbols.delete_sweep),
 | |
|                       label: Text('clearAll'.tr()),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
|           ],
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _DraftItem extends StatelessWidget {
 | |
|   final dynamic draft;
 | |
|   final VoidCallback? onTap;
 | |
|   final VoidCallback? onDelete;
 | |
| 
 | |
|   const _DraftItem({required this.draft, this.onTap, this.onDelete});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final theme = Theme.of(context);
 | |
|     final colorScheme = theme.colorScheme;
 | |
| 
 | |
|     final title = draft.title ?? 'untitled'.tr();
 | |
|     final content = draft.content ?? (draft.description ?? 'noContent'.tr());
 | |
|     final preview =
 | |
|         content.length > 100 ? '${content.substring(0, 100)}...' : content;
 | |
|     final timeAgo = _formatTimeAgo(draft.updatedAt!);
 | |
|     final visibility = _parseVisibility(draft.visibility).tr();
 | |
| 
 | |
|     return Card(
 | |
|       margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
 | |
|       child: InkWell(
 | |
|         onTap: onTap,
 | |
|         borderRadius: BorderRadius.circular(12),
 | |
|         child: Padding(
 | |
|           padding: const EdgeInsets.all(16),
 | |
|           child: Column(
 | |
|             crossAxisAlignment: CrossAxisAlignment.start,
 | |
|             children: [
 | |
|               Row(
 | |
|                 children: [
 | |
|                   Icon(
 | |
|                     draft.type == 1 ? Symbols.article : Symbols.post_add,
 | |
|                     size: 20,
 | |
|                     color: colorScheme.primary,
 | |
|                   ),
 | |
|                   const Gap(8),
 | |
|                   Expanded(
 | |
|                     child: Text(
 | |
|                       title,
 | |
|                       style: theme.textTheme.titleMedium?.copyWith(
 | |
|                         fontWeight: FontWeight.w600,
 | |
|                       ),
 | |
|                       maxLines: 1,
 | |
|                       overflow: TextOverflow.ellipsis,
 | |
|                     ),
 | |
|                   ),
 | |
|                   IconButton(
 | |
|                     onPressed: onDelete,
 | |
|                     icon: const Icon(Symbols.delete),
 | |
|                     iconSize: 20,
 | |
|                     visualDensity: VisualDensity.compact,
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|               if (preview.isNotEmpty) ...[
 | |
|                 const Gap(8),
 | |
|                 Text(
 | |
|                   preview,
 | |
|                   style: theme.textTheme.bodyMedium?.copyWith(
 | |
|                     color: colorScheme.onSurface.withOpacity(0.7),
 | |
|                   ),
 | |
|                   maxLines: 2,
 | |
|                   overflow: TextOverflow.ellipsis,
 | |
|                 ),
 | |
|               ],
 | |
|               const Gap(8),
 | |
|               Row(
 | |
|                 children: [
 | |
|                   Icon(
 | |
|                     Symbols.schedule,
 | |
|                     size: 16,
 | |
|                     color: colorScheme.onSurface.withOpacity(0.5),
 | |
|                   ),
 | |
|                   const Gap(4),
 | |
|                   Text(
 | |
|                     timeAgo,
 | |
|                     style: theme.textTheme.bodySmall?.copyWith(
 | |
|                       color: colorScheme.onSurface.withOpacity(0.5),
 | |
|                     ),
 | |
|                   ),
 | |
|                   const Spacer(),
 | |
|                   Container(
 | |
|                     padding: const EdgeInsets.symmetric(
 | |
|                       horizontal: 8,
 | |
|                       vertical: 2,
 | |
|                     ),
 | |
|                     decoration: BoxDecoration(
 | |
|                       color: colorScheme.primaryContainer,
 | |
|                       borderRadius: BorderRadius.circular(12),
 | |
|                     ),
 | |
|                     child: Text(
 | |
|                       visibility,
 | |
|                       style: theme.textTheme.bodySmall?.copyWith(
 | |
|                         color: colorScheme.onPrimaryContainer,
 | |
|                         fontWeight: FontWeight.w500,
 | |
|                       ),
 | |
|                     ),
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ],
 | |
|           ),
 | |
|         ),
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   String _formatTimeAgo(DateTime dateTime) {
 | |
|     final now = DateTime.now();
 | |
|     final difference = now.difference(dateTime);
 | |
| 
 | |
|     if (difference.inMinutes < 1) {
 | |
|       return 'justNow'.tr();
 | |
|     } else if (difference.inHours < 1) {
 | |
|       return 'minutesAgo'.tr(args: [difference.inMinutes.toString()]);
 | |
|     } else if (difference.inDays < 1) {
 | |
|       return 'hoursAgo'.tr(args: [difference.inHours.toString()]);
 | |
|     } else if (difference.inDays < 7) {
 | |
|       return 'daysAgo'.tr(args: [difference.inDays.toString()]);
 | |
|     } else {
 | |
|       return DateFormat('MMM dd, yyyy').format(dateTime);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   String _parseVisibility(int visibility) {
 | |
|     switch (visibility) {
 | |
|       case 1:
 | |
|         return 'postVisibilityFriends';
 | |
|       case 2:
 | |
|         return 'postVisibilityUnlisted';
 | |
|       case 3:
 | |
|         return 'postVisibilityPrivate';
 | |
|       default:
 | |
|         return 'postVisibilityPublic';
 | |
|     }
 | |
|   }
 | |
| }
 |