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]);
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |