import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:neon/blocs.dart'; import 'package:neon/utils.dart'; import 'package:neon/widgets.dart'; import 'package:neon_news/l10n/localizations.dart'; import 'package:neon_news/neon_news.dart'; import 'package:nextcloud/news.dart' as news; /// A dialog for adding a news feed by url. /// /// When created a record with `(String url, int? folderId)` will be popped. class NewsAddFeedDialog extends StatefulWidget { /// Creates a new add feed dialog. const NewsAddFeedDialog({ required this.bloc, this.folderID, super.key, }); /// The active client bloc. final NewsBloc bloc; /// The initial id of the folder the feed is in. final int? folderID; @override State createState() => _NewsAddFeedDialogState(); } class _NewsAddFeedDialogState extends State { final formKey = GlobalKey(); final controller = TextEditingController(); news.Folder? folder; void submit() { if (formKey.currentState!.validate()) { Navigator.of(context).pop((controller.text, widget.folderID ?? folder?.id)); } } @override void initState() { super.initState(); unawaited( Clipboard.getData(Clipboard.kTextPlain).then((final clipboardContent) { if (clipboardContent != null && clipboardContent.text != null) { final uri = Uri.tryParse(clipboardContent.text!); if (uri != null && (uri.scheme == 'http' || uri.scheme == 'https')) { controller.text = clipboardContent.text!; } } }), ); } @override void dispose() { controller.dispose(); super.dispose(); } @override Widget build(final BuildContext context) { final urlField = Form( key: formKey, child: TextFormField( autofocus: true, controller: controller, decoration: const InputDecoration( hintText: 'https://...', ), keyboardType: TextInputType.url, validator: (final input) => validateHttpUrl(context, input), onFieldSubmitted: (final _) { submit(); }, autofillHints: const [AutofillHints.url], ), ); final folderSelector = ResultBuilder>.behaviorSubject( subject: widget.bloc.folders, builder: (final context, final folders) { if (folders.hasError) { return Center( child: NeonError( folders.error, onRetry: widget.bloc.refresh, ), ); } if (!folders.hasData) { return Center( child: NeonLinearProgressIndicator( visible: folders.isLoading, ), ); } return NewsFolderSelect( folders: folders.requireData, value: folder, onChanged: (final f) { setState(() { folder = f; }); }, ); }, ); return NeonDialog( title: Text(NewsLocalizations.of(context).feedAdd), content: Material( child: Column( mainAxisSize: MainAxisSize.min, children: [ urlField, const SizedBox(height: 8), folderSelector, ], ), ), actions: [ NeonDialogAction( isDefaultAction: true, onPressed: submit, child: Text( NewsLocalizations.of(context).feedAdd, textAlign: TextAlign.end, ), ), ], ); } } /// A dialog for displaying the url of a news feed. class NewsFeedShowURLDialog extends StatelessWidget { /// Creates a new display url dialog. const NewsFeedShowURLDialog({ required this.feed, super.key, }); /// The feed to display the url for. final news.Feed feed; @override Widget build(final BuildContext context) => NeonDialog( title: Text(feed.url), actions: [ NeonDialogAction( onPressed: () { Navigator.of(context).pop(); }, child: Text( NeonLocalizations.of(context).actionClose, textAlign: TextAlign.end, ), ), NeonDialogAction( isDefaultAction: true, onPressed: () async { await Clipboard.setData( ClipboardData( text: feed.url, ), ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(NewsLocalizations.of(context).feedCopiedURL), ), ); Navigator.of(context).pop(); } }, child: Text( NewsLocalizations.of(context).feedCopyURL, textAlign: TextAlign.end, ), ), ], ); } class NewsFeedUpdateErrorDialog extends StatelessWidget { const NewsFeedUpdateErrorDialog({ required this.feed, super.key, }); final news.Feed feed; @override Widget build(final BuildContext context) => NeonDialog( title: Text(feed.lastUpdateError!), actions: [ NeonDialogAction( onPressed: () { Navigator.of(context).pop(); }, child: Text( NeonLocalizations.of(context).actionClose, textAlign: TextAlign.end, ), ), NeonDialogAction( isDefaultAction: true, onPressed: () async { await Clipboard.setData( ClipboardData( text: feed.lastUpdateError!, ), ); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(NewsLocalizations.of(context).feedCopiedErrorMessage), ), ); Navigator.of(context).pop(); } }, child: Text( NewsLocalizations.of(context).feedCopyErrorMessage, textAlign: TextAlign.end, ), ), ], ); } /// A dialog for moving a news feed by into a different folder. /// /// When moved the id of the new folder will be popped. class NewsMoveFeedDialog extends StatefulWidget { /// Creates a new move feed dialog. const NewsMoveFeedDialog({ required this.folders, required this.feed, super.key, }); /// The list of available folders. final List folders; /// The feed to move. final news.Feed feed; @override State createState() => _NewsMoveFeedDialogState(); } class _NewsMoveFeedDialogState extends State { final formKey = GlobalKey(); news.Folder? folder; void submit() { if (formKey.currentState!.validate()) { Navigator.of(context).pop(folder?.id); } } @override void initState() { folder = widget.folders.singleWhereOrNull((final folder) => folder.id == widget.feed.folderId); super.initState(); } @override Widget build(final BuildContext context) => NeonDialog( title: Text(NewsLocalizations.of(context).feedMove), content: Material( child: Form( key: formKey, child: NewsFolderSelect( folders: widget.folders, value: folder, onChanged: (final f) { setState(() { folder = f; }); }, ), ), ), actions: [ NeonDialogAction( isDefaultAction: true, onPressed: submit, child: Text( NewsLocalizations.of(context).feedMove, textAlign: TextAlign.end, ), ), ], ); } /// A [NeonDialog] that shows for renaming creating a new folder. /// /// Use `showFolderCreateDialog` to display this dialog. /// /// When submitted the folder name will be popped as a `String`. class NewsCreateFolderDialog extends StatefulWidget { /// Creates a new NeonDialog for creating a folder. const NewsCreateFolderDialog({ super.key, }); @override State createState() => _NewsCreateFolderDialogState(); } class _NewsCreateFolderDialogState extends State { final formKey = GlobalKey(); final controller = TextEditingController(); @override void dispose() { controller.dispose(); super.dispose(); } void submit() { if (formKey.currentState!.validate()) { Navigator.of(context).pop(controller.text); } } @override Widget build(final BuildContext context) { final content = Material( child: TextFormField( controller: controller, decoration: InputDecoration( hintText: NewsLocalizations.of(context).folderCreateName, ), autofocus: true, validator: (final input) => validateNotEmpty(context, input), onFieldSubmitted: (final _) { submit(); }, ), ); return NeonDialog( title: Text(NewsLocalizations.of(context).folderCreate), content: Form( key: formKey, child: content, ), actions: [ NeonDialogAction( isDefaultAction: true, onPressed: submit, child: Text( NewsLocalizations.of(context).folderCreate, textAlign: TextAlign.end, ), ), ], ); } }