263 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			263 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.dart';
 | |
| import 'package:go_router/go_router.dart';
 | |
| import 'package:hooks_riverpod/hooks_riverpod.dart';
 | |
| import 'package:island/models/webfeed.dart';
 | |
| import 'package:island/pods/webfeed.dart';
 | |
| import 'package:island/widgets/alert.dart';
 | |
| import 'package:island/widgets/response.dart';
 | |
| import 'package:material_symbols_icons/symbols.dart';
 | |
| import 'package:styled_widget/styled_widget.dart';
 | |
| 
 | |
| class WebfeedForm extends HookConsumerWidget {
 | |
|   final String pubName;
 | |
|   final String? feedId;
 | |
| 
 | |
|   const WebfeedForm({super.key, required this.pubName, this.feedId});
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context, WidgetRef ref) {
 | |
|     final formKey = useMemoized(() => GlobalKey<FormState>());
 | |
|     final titleController = useTextEditingController();
 | |
|     final urlController = useTextEditingController();
 | |
|     final descriptionController = useTextEditingController();
 | |
|     final isLoading = useState(false);
 | |
|     final isScrapEnabled = useState(false);
 | |
| 
 | |
|     final saveFeed = useCallback(() async {
 | |
|       if (!formKey.currentState!.validate()) return;
 | |
| 
 | |
|       isLoading.value = true;
 | |
| 
 | |
|       try {
 | |
|         final feed = SnWebFeed(
 | |
|           id: feedId ?? '',
 | |
|           title: titleController.text,
 | |
|           url: urlController.text,
 | |
|           description: descriptionController.text,
 | |
|           config: SnWebFeedConfig(scrapPage: isScrapEnabled.value),
 | |
|           publisherId: pubName,
 | |
|           createdAt: DateTime.now(),
 | |
|           updatedAt: DateTime.now(),
 | |
|           deletedAt: null,
 | |
|         );
 | |
| 
 | |
|         await ref
 | |
|             .read(
 | |
|               webFeedNotifierProvider((
 | |
|                 pubName: pubName,
 | |
|                 feedId: feedId,
 | |
|               )).notifier,
 | |
|             )
 | |
|             .saveFeed(feed);
 | |
| 
 | |
|         // Refresh the feed list
 | |
|         ref.invalidate(webFeedListProvider(pubName));
 | |
| 
 | |
|         if (context.mounted) {
 | |
|           showSnackBar('Web feed saved successfully');
 | |
|           context.pop();
 | |
|         }
 | |
|       } catch (e) {
 | |
|         showErrorAlert(e);
 | |
|       } finally {
 | |
|         isLoading.value = false;
 | |
|       }
 | |
|     }, [pubName, feedId, isScrapEnabled.value, context]);
 | |
| 
 | |
|     final deleteFeed = useCallback(() async {
 | |
|       final confirmed = await showConfirmAlert(
 | |
|         'Are you sure you want to delete this web feed? This action cannot be undone.',
 | |
|         'Delete Web Feed',
 | |
|       );
 | |
|       if (confirmed != true) return;
 | |
| 
 | |
|       isLoading.value = true;
 | |
| 
 | |
|       try {
 | |
|         await ref
 | |
|             .read(
 | |
|               webFeedNotifierProvider((
 | |
|                 pubName: pubName,
 | |
|                 feedId: feedId!,
 | |
|               )).notifier,
 | |
|             )
 | |
|             .deleteFeed();
 | |
| 
 | |
|         ref.invalidate(webFeedListProvider(pubName));
 | |
| 
 | |
|         if (context.mounted) {
 | |
|           showSnackBar('Web feed deleted successfully');
 | |
|           context.pop();
 | |
|         }
 | |
|       } catch (e) {
 | |
|         showErrorAlert(e);
 | |
|       } finally {
 | |
|         isLoading.value = false;
 | |
|       }
 | |
|     }, [pubName, feedId, context, ref]);
 | |
| 
 | |
|     final feedAsync = ref.watch(
 | |
|       webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
 | |
|     );
 | |
| 
 | |
|     return feedAsync.when(
 | |
|       loading: () => const Center(child: CircularProgressIndicator()),
 | |
|       error:
 | |
|           (error, _) => ResponseErrorWidget(
 | |
|             error: error,
 | |
|             onRetry:
 | |
|                 () => ref.invalidate(
 | |
|                   webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
 | |
|                 ),
 | |
|           ),
 | |
|       data: (feed) {
 | |
|         // Initialize form fields if they're empty and we have a feed
 | |
|         if (titleController.text.isEmpty) {
 | |
|           titleController.text = feed.title;
 | |
|           urlController.text = feed.url;
 | |
|           descriptionController.text = feed.description ?? '';
 | |
|           isScrapEnabled.value = feed.config.scrapPage;
 | |
|         }
 | |
| 
 | |
|         final scrapNow = useCallback(() async {
 | |
|           isLoading.value = true;
 | |
|           try {
 | |
|             await ref
 | |
|                 .read(
 | |
|                   webFeedNotifierProvider((
 | |
|                     pubName: pubName,
 | |
|                     feedId: feedId!,
 | |
|                   )).notifier,
 | |
|                 )
 | |
|                 .scrapFeed();
 | |
| 
 | |
|             if (context.mounted) {
 | |
|               showSnackBar('Feed scraping successfully.');
 | |
|             }
 | |
|           } catch (e) {
 | |
|             showErrorAlert(e);
 | |
|           } finally {
 | |
|             isLoading.value = false;
 | |
|           }
 | |
|         }, [pubName, feedId, ref, context, isLoading]);
 | |
| 
 | |
|         final formFields = Column(
 | |
|           children: [
 | |
|             TextFormField(
 | |
|               controller: titleController,
 | |
|               decoration: const InputDecoration(
 | |
|                 labelText: 'Title',
 | |
|                 border: OutlineInputBorder(
 | |
|                   borderRadius: BorderRadius.all(Radius.circular(12)),
 | |
|                 ),
 | |
|               ),
 | |
|               validator: (value) {
 | |
|                 if (value == null || value.isEmpty) {
 | |
|                   return 'Please enter a title';
 | |
|                 }
 | |
|                 return null;
 | |
|               },
 | |
|               onTapOutside:
 | |
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | |
|             ),
 | |
|             const SizedBox(height: 16),
 | |
|             TextFormField(
 | |
|               controller: urlController,
 | |
|               decoration: const InputDecoration(
 | |
|                 labelText: 'URL',
 | |
|                 hintText: 'https://example.com/feed',
 | |
|                 border: OutlineInputBorder(
 | |
|                   borderRadius: BorderRadius.all(Radius.circular(12)),
 | |
|                 ),
 | |
|               ),
 | |
|               keyboardType: TextInputType.url,
 | |
|               validator: (value) {
 | |
|                 if (value == null || value.isEmpty) {
 | |
|                   return 'Please enter a URL';
 | |
|                 }
 | |
|                 final uri = Uri.tryParse(value);
 | |
|                 if (uri == null || !uri.hasAbsolutePath) {
 | |
|                   return 'Please enter a valid URL';
 | |
|                 }
 | |
|                 return null;
 | |
|               },
 | |
|               onTapOutside:
 | |
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | |
|             ),
 | |
|             const SizedBox(height: 16),
 | |
|             TextFormField(
 | |
|               controller: descriptionController,
 | |
|               decoration: const InputDecoration(
 | |
|                 labelText: 'Description',
 | |
|                 alignLabelWithHint: true,
 | |
|                 border: OutlineInputBorder(
 | |
|                   borderRadius: BorderRadius.all(Radius.circular(12)),
 | |
|                 ),
 | |
|               ),
 | |
|               onTapOutside:
 | |
|                   (_) => FocusManager.instance.primaryFocus?.unfocus(),
 | |
|               maxLines: 3,
 | |
|             ),
 | |
|             const SizedBox(height: 24),
 | |
|             Card(
 | |
|               margin: EdgeInsets.zero,
 | |
|               child: Column(
 | |
|                 crossAxisAlignment: CrossAxisAlignment.start,
 | |
|                 children: [
 | |
|                   SwitchListTile(
 | |
|                     title: const Text('Scrape web page for content'),
 | |
|                     subtitle: const Text(
 | |
|                       'When enabled, the system will attempt to extract full content from the web page',
 | |
|                     ),
 | |
|                     shape: RoundedRectangleBorder(
 | |
|                       borderRadius: BorderRadius.circular(8),
 | |
|                     ),
 | |
|                     value: isScrapEnabled.value,
 | |
|                     onChanged: (value) => isScrapEnabled.value = value,
 | |
|                   ),
 | |
|                 ],
 | |
|               ),
 | |
|             ),
 | |
|             const SizedBox(height: 20),
 | |
|             if (feedId != null) ...[
 | |
|               TextButton.icon(
 | |
|                 onPressed: isLoading.value ? null : scrapNow,
 | |
|                 icon: const Icon(Symbols.refresh),
 | |
|                 label: const Text('Scrape Now'),
 | |
|               ).alignment(Alignment.centerRight),
 | |
|               const SizedBox(height: 16),
 | |
|             ],
 | |
|           ],
 | |
|         ).padding(all: 20);
 | |
| 
 | |
|         final formWidget = Form(
 | |
|           key: formKey,
 | |
|           child: SingleChildScrollView(child: formFields),
 | |
|         );
 | |
| 
 | |
|         final buttonsRow = Row(
 | |
|           children: [
 | |
|             if (feedId != null)
 | |
|               TextButton.icon(
 | |
|                 onPressed: isLoading.value ? null : deleteFeed,
 | |
|                 icon: const Icon(Symbols.delete_forever),
 | |
|                 label: const Text('Delete Web Feed'),
 | |
|                 style: TextButton.styleFrom(foregroundColor: Colors.red),
 | |
|               ),
 | |
|             const Spacer(),
 | |
|             TextButton.icon(
 | |
|               onPressed: isLoading.value ? null : saveFeed,
 | |
|               icon: const Icon(Symbols.save),
 | |
|               label: Text('saveChanges').tr(),
 | |
|             ),
 | |
|           ],
 | |
|         ).padding(horizontal: 20, vertical: 12);
 | |
| 
 | |
|         return Column(children: [Expanded(child: formWidget), buttonsRow]);
 | |
|       },
 | |
|     );
 | |
|   }
 | |
| }
 |